diff --git a/backend/package.json b/backend/package.json index 0d85d371b..267aff164 100644 --- a/backend/package.json +++ b/backend/package.json @@ -83,11 +83,11 @@ "metascraper-lang-detector": "^4.8.5", "metascraper-logo": "^5.6.5", "metascraper-publisher": "^5.6.5", - "metascraper-soundcloud": "^5.6.5", + "metascraper-soundcloud": "^5.6.7", "metascraper-title": "^5.6.5", "metascraper-url": "^5.6.5", "metascraper-video": "^5.6.5", - "metascraper-youtube": "^5.6.3", + "metascraper-youtube": "^5.6.7", "minimatch": "^3.0.4", "neo4j-driver": "~1.7.6", "neo4j-graphql-js": "^2.7.2", @@ -100,6 +100,7 @@ "slug": "~1.1.0", "trunc-html": "~1.1.2", "uuid": "~3.3.3", + "xregexp": "^4.2.4", "wait-on": "~3.3.0" }, "devDependencies": { @@ -130,4 +131,4 @@ "prettier": "~1.18.2", "supertest": "~4.0.2" } -} +} \ No newline at end of file diff --git a/backend/src/middleware/dateTimeMiddleware.js b/backend/src/middleware/dateTimeMiddleware.js index ff1fcc996..e8bd27306 100644 --- a/backend/src/middleware/dateTimeMiddleware.js +++ b/backend/src/middleware/dateTimeMiddleware.js @@ -11,10 +11,8 @@ export default { Mutation: { CreatePost: setCreatedAt, CreateComment: setCreatedAt, - CreateOrganization: setCreatedAt, UpdateUser: setUpdatedAt, UpdatePost: setUpdatedAt, UpdateComment: setUpdatedAt, - UpdateOrganization: setUpdatedAt, }, } diff --git a/backend/src/middleware/excerptMiddleware.js b/backend/src/middleware/excerptMiddleware.js index 3b3a27c2c..40a6a6ae4 100644 --- a/backend/src/middleware/excerptMiddleware.js +++ b/backend/src/middleware/excerptMiddleware.js @@ -22,15 +22,5 @@ export default { const result = await resolve(root, args, context, info) return result }, - CreateOrganization: async (resolve, root, args, context, info) => { - args.descriptionExcerpt = trunc(args.description, 120).html - const result = await resolve(root, args, context, info) - return result - }, - UpdateOrganization: async (resolve, root, args, context, info) => { - args.descriptionExcerpt = trunc(args.description, 120).html - const result = await resolve(root, args, context, info) - return result - }, }, } diff --git a/backend/src/middleware/hashtags/extractHashtags.js b/backend/src/middleware/hashtags/extractHashtags.js index fd6613065..c7782e59d 100644 --- a/backend/src/middleware/hashtags/extractHashtags.js +++ b/backend/src/middleware/hashtags/extractHashtags.js @@ -1,17 +1,18 @@ import cheerio from 'cheerio' +import { exec, build } from 'xregexp/xregexp-all.js' // formats of a Hashtag: // https://en.wikipedia.org/w/index.php?title=Hashtag&oldid=905141980#Style // here: // 0. Search for whole string. -// 1. Hashtag has only 'a-z', 'A-Z', and '0-9'. -// 2. If it starts with a digit '0-9' than 'a-z', or 'A-Z' has to follow. -const ID_REGEX = /^\/search\/hashtag\/(([a-zA-Z]+[a-zA-Z0-9]*)|([0-9]+[a-zA-Z]+[a-zA-Z0-9]*))$/g +// 1. Hashtag has only all unicode characters and '0-9'. +// 2. If it starts with a digit '0-9' than a unicode character has to follow. +const regX = build('^/search/hashtag/((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$') export default function(content) { if (!content) return [] const $ = cheerio.load(content) // We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware. - // But we have to know, which Hashtags are removed from the content es well, so we search for the 'a' html-tag. + // But we have to know, which Hashtags are removed from the content as well, so we search for the 'a' html-tag. const urls = $('a') .map((_, el) => { return $(el).attr('href') @@ -19,8 +20,8 @@ export default function(content) { .get() const hashtags = [] urls.forEach(url => { - let match - while ((match = ID_REGEX.exec(url)) != null) { + const match = exec(url, regX) + if (match != null) { hashtags.push(match[1]) } }) diff --git a/backend/src/middleware/hashtags/extractHashtags.spec.js b/backend/src/middleware/hashtags/extractHashtags.spec.js index eb581d8f5..2e1761718 100644 --- a/backend/src/middleware/hashtags/extractHashtags.spec.js +++ b/backend/src/middleware/hashtags/extractHashtags.spec.js @@ -28,9 +28,14 @@ describe('extractHashtags', () => { }) it('ignores Hashtag links with not allowed character combinations', () => { + // Allowed are all unicode letters '\pL' and all digits '0-9'. There haveto be at least one letter in it. const content = - '

Something inspirational about #AbcDefXyz0123456789!*(),2, #0123456789, #0123456789a and #AbcDefXyz0123456789.

' - expect(extractHashtags(content)).toEqual(['0123456789a', 'AbcDefXyz0123456789']) + '

Something inspirational about #AbcDefXyz0123456789!*(),2, #0123456789, #0123456789a, #AbcDefXyz0123456789, and #λαπ.

' + expect(extractHashtags(content).sort()).toEqual([ + '0123456789a', + 'AbcDefXyz0123456789', + 'λαπ', + ]) }) }) diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index 6133a3c14..03d7f8584 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -25,10 +25,6 @@ export default { args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) }, - CreateOrganization: async (resolve, root, args, context, info) => { - args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Organization'))) - return resolve(root, args, context, info) - }, CreateCategory: async (resolve, root, args, context, info) => { args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Category'))) return resolve(root, args, context, info) diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js index 0f42def85..d11757bc7 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.js +++ b/backend/src/middleware/slugifyMiddleware.spec.js @@ -89,8 +89,10 @@ describe('slugify', () => { }) describe('SignupVerification', () => { - const mutation = `mutation($password: String!, $email: String!, $name: String!, $slug: String, $nonce: String!) { - SignupVerification(email: $email, password: $password, name: $name, slug: $slug, nonce: $nonce) { slug } + const mutation = `mutation($password: String!, $email: String!, $name: String!, $slug: String, $nonce: String!, $termsAndConditionsAgreedVersion: String!) { + SignupVerification(email: $email, password: $password, name: $name, slug: $slug, nonce: $nonce, termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion) { + slug + } } ` @@ -98,7 +100,12 @@ describe('slugify', () => { // required for SignupVerification await instance.create('EmailAddress', { email: '123@example.org', nonce: '123456' }) - const defaultVariables = { nonce: '123456', password: 'yo', email: '123@example.org' } + const defaultVariables = { + nonce: '123456', + password: 'yo', + email: '123@example.org', + termsAndConditionsAgreedVersion: '0.0.1', + } return authenticatedClient.request(mutation, { ...defaultVariables, ...variables }) } diff --git a/backend/src/models/User.js b/backend/src/models/User.js index 28ab46d3c..c2749ce6a 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -80,9 +80,19 @@ module.exports = { notifications: { type: 'relationship', relationship: 'NOTIFIED', - target: 'Notification', + target: 'User', direction: 'in', }, + termsAndConditionsAgreedVersion: { + type: 'string', + allow: [null], + }, + /* termsAndConditionsAgreedAt: { + type: 'string', + isoDate: true, + allow: [null], + // required: true, TODO + }, */ shouted: { type: 'relationship', relationship: 'SHOUTED', diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index e8fa63d97..4d1b9574e 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -14,11 +14,13 @@ export default applyScalars( query: { exclude: [ 'Badge', + 'Embed', 'InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', 'LoggedInUser', + 'Location', 'SocialMedia', 'NOTIFIED', ], @@ -27,12 +29,19 @@ export default applyScalars( mutation: { exclude: [ 'Badge', + 'Embed', 'InvitationCode', 'EmailAddress', 'Notfication', + 'Post', + 'Comment', + 'Report', 'Statistics', 'LoggedInUser', + 'Location', 'SocialMedia', + 'User', + 'EMOTED', 'NOTIFIED', ], // add 'User' here as soon as possible diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index 20c54a49b..423ce7580 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -1,4 +1,4 @@ -import { UserInputError } from 'apollo-server' +import { ForbiddenError, UserInputError } from 'apollo-server' import uuid from 'uuid/v4' import { neode } from '../../bootstrap/neo4j' import fileUpload from './fileUpload' @@ -77,6 +77,12 @@ export default { } }, SignupVerification: async (object, args, context, resolveInfo) => { + const { termsAndConditionsAgreedVersion } = args + const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g) + if (!regEx.test(termsAndConditionsAgreedVersion)) { + throw new ForbiddenError('Invalid version format!') + } + let { nonce, email } = args email = email.toLowerCase() const result = await instance.cypher( diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index 9f9a171f7..8e33bf314 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -34,6 +34,7 @@ describe('CreateInvitationCode', () => { name: 'Inviter', email: 'inviter@example.org', password: '1234', + termsAndConditionsAgreedVersion: '0.0.1', } action = async () => { const factory = Factory() @@ -293,19 +294,25 @@ describe('Signup', () => { describe('SignupVerification', () => { const mutation = ` - mutation($name: String!, $password: String!, $email: String!, $nonce: String!) { - SignupVerification(name: $name, password: $password, email: $email, nonce: $nonce) { + mutation($name: String!, $password: String!, $email: String!, $nonce: String!, $termsAndConditionsAgreedVersion: String!) { + SignupVerification(name: $name, password: $password, email: $email, nonce: $nonce, termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion) { id + termsAndConditionsAgreedVersion } } ` describe('given valid password and email', () => { - const variables = { - nonce: '123456', - name: 'John Doe', - password: '123', - email: 'john@example.org', - } + let variables + + beforeEach(async () => { + variables = { + nonce: '123456', + name: 'John Doe', + password: '123', + email: 'john@example.org', + termsAndConditionsAgreedVersion: '0.0.1', + } + }) describe('unauthenticated', () => { beforeEach(async () => { @@ -349,9 +356,9 @@ describe('SignupVerification', () => { describe('sending a valid nonce', () => { it('creates a user account', async () => { const expected = { - SignupVerification: { + SignupVerification: expect.objectContaining({ id: expect.any(String), - }, + }), } await expect(client.request(mutation, variables)).resolves.toEqual(expected) }) @@ -385,6 +392,24 @@ describe('SignupVerification', () => { const { records: emails } = await instance.cypher(cypher, { name: 'John Doe' }) expect(emails).toHaveLength(1) }) + + it('is version of terms and conditions saved correctly', async () => { + const expected = { + SignupVerification: expect.objectContaining({ + termsAndConditionsAgreedVersion: '0.0.1', + }), + } + await expect(client.request(mutation, variables)).resolves.toEqual(expected) + }) + + it('rejects if version of terms and conditions has wrong format', async () => { + await expect( + client.request(mutation, { + ...variables, + termsAndConditionsAgreedVersion: 'invalid version format', + }), + ).rejects.toThrow('Invalid version format!') + }) }) describe('sending invalid nonce', () => { diff --git a/backend/src/schema/resolvers/statistics.js b/backend/src/schema/resolvers/statistics.js index 982c2acfa..db6e205d2 100644 --- a/backend/src/schema/resolvers/statistics.js +++ b/backend/src/schema/resolvers/statistics.js @@ -43,14 +43,8 @@ export default { 'MATCH (r:Post) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countPosts', countComments: 'MATCH (r:Comment) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countComments', - countNotifications: - 'MATCH (r:Notification) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countNotifications', - countOrganizations: - 'MATCH (r:Organization) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countOrganizations', - countProjects: - 'MATCH (r:Project) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countProjects', - countInvites: - 'MATCH (r:Invite) WHERE r.wasUsed <> true OR NOT exists(r.wasUsed) RETURN COUNT(r) AS countInvites', + countNotifications: 'MATCH ()-[r:NOTIFIED]->() RETURN COUNT(r) AS countNotifications', + countInvites: 'MATCH (r:InvitationCode) RETURN COUNT(r) AS countInvites', countFollows: 'MATCH (:User)-[r:FOLLOWS]->(:User) RETURN COUNT(r) AS countFollows', countShouts: 'MATCH (:User)-[r:SHOUTED]->(:Post) RETURN COUNT(r) AS countShouts', } @@ -63,12 +57,6 @@ export default { countNotifications: queryOne(queries.countNotifications, session).then( res => res.countNotifications.low, ), - countOrganizations: queryOne(queries.countOrganizations, session).then( - res => res.countOrganizations.low, - ), - countProjects: queryOne(queries.countProjects, session).then( - res => res.countProjects.low, - ), countInvites: queryOne(queries.countInvites, session).then(res => res.countInvites.low), countFollows: queryOne(queries.countFollows, session).then(res => res.countFollows.low), countShouts: queryOne(queries.countShouts, session).then(res => res.countShouts.low), diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index a634eaf85..e1528cc9e 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -1,7 +1,6 @@ import encode from '../../jwt/encode' import bcrypt from 'bcryptjs' import { AuthenticationError } from 'apollo-server' -import { neo4jgraphql } from 'neo4j-graphql-js' import { neode } from '../../bootstrap/neo4j' const instance = neode() @@ -12,9 +11,9 @@ export default { return Boolean(user && user.id) }, currentUser: async (object, params, ctx, resolveInfo) => { - const { user } = ctx - if (!user) return null - return neo4jgraphql(object, { id: user.id }, ctx, resolveInfo, false) + if (!ctx.user) return null + const user = await instance.find('User', ctx.user.id) + return user.toJson() }, }, Mutation: { diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 44d4cff50..c7afee7c8 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -1,7 +1,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import fileUpload from './fileUpload' import { neode } from '../../bootstrap/neo4j' -import { UserInputError } from 'apollo-server' +import { UserInputError, ForbiddenError } from 'apollo-server' import Resolver from './helpers/Resolver' const instance = neode() @@ -88,6 +88,13 @@ export default { return blockedUser.toJson() }, UpdateUser: async (object, args, context, resolveInfo) => { + const { termsAndConditionsAgreedVersion } = args + if (termsAndConditionsAgreedVersion) { + const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g) + if (!regEx.test(termsAndConditionsAgreedVersion)) { + throw new ForbiddenError('Invalid version format!') + } + } args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' }) try { const user = await instance.find('User', args.id) @@ -158,6 +165,7 @@ export default { }, ...Resolver('User', { undefinedToNull: [ + 'termsAndConditionsAgreedVersion', 'actorId', 'avatar', 'coverImg', @@ -165,6 +173,8 @@ export default { 'disabled', 'locationName', 'about', + 'termsAndConditionsAgreedVersion', + // TODO: 'termsAndConditionsAgreedAt', ], boolean: { followedByCurrentUser: @@ -197,8 +207,6 @@ export default { contributions: '-[:WROTE]->(related:Post)', comments: '-[:WROTE]->(related:Comment)', shouted: '-[:SHOUTED]->(related:Post)', - organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)', - organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)', categories: '-[:CATEGORIZED]->(related:Category)', badges: '<-[:REWARDED]-(related:Badge)', }, diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index e8e6205ca..50f413157 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -75,22 +75,33 @@ describe('User', () => { }) describe('UpdateUser', () => { - const userParams = { - email: 'user@example.org', - password: '1234', - id: 'u47', - name: 'John Doe', - } - const variables = { - id: 'u47', - name: 'John Doughnut', - } + let userParams + let variables + + beforeEach(async () => { + userParams = { + email: 'user@example.org', + password: '1234', + id: 'u47', + name: 'John Doe', + } + + variables = { + id: 'u47', + name: 'John Doughnut', + } + }) const updateUserMutation = gql` - mutation($id: ID!, $name: String) { - UpdateUser(id: $id, name: $name) { + mutation($id: ID!, $name: String, $termsAndConditionsAgreedVersion: String) { + UpdateUser( + id: $id + name: $name + termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion + ) { id name + termsAndConditionsAgreedVersion } } ` @@ -159,6 +170,30 @@ describe('UpdateUser', () => { 'child "name" fails because ["name" length must be at least 3 characters long]', ) }) + + it('given a new agreed version of terms and conditions', async () => { + variables = { ...variables, termsAndConditionsAgreedVersion: '0.0.2' } + const expected = { + data: { + UpdateUser: expect.objectContaining({ + termsAndConditionsAgreedVersion: '0.0.2', + }), + }, + } + + await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject( + expected, + ) + }) + + it('rejects if version of terms and conditions has wrong format', async () => { + variables = { + ...variables, + termsAndConditionsAgreedVersion: 'invalid version format', + } + const { errors } = await mutate({ mutation: updateUserMutation, variables }) + expect(errors[0]).toHaveProperty('message', 'Invalid version format!') + }) }) }) diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index dc2cec8f5..7aa04ea57 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -44,8 +44,6 @@ type Statistics { countPosts: Int! countComments: Int! countNotifications: Int! - countOrganizations: Int! - countProjects: Int! countInvites: Int! countFollows: Int! countShouts: Int! @@ -70,13 +68,9 @@ enum Deletable { enum ShoutTypeEnum { Post - Organization - Project } enum FollowTypeEnum { User - Organization - Project } type Reward { @@ -87,21 +81,6 @@ type Reward { badge: Badge @relation(name: "REWARDED", direction: "OUT") } -type Organization { - id: ID! - createdBy: User @relation(name: "CREATED_ORGA", direction: "IN") - ownedBy: [User] @relation(name: "OWNING_ORGA", direction: "IN") - name: String! - slug: String - description: String! - descriptionExcerpt: String - deleted: Boolean - disabled: Boolean - - tags: [Tag]! @relation(name: "TAGGED", direction: "OUT") - categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") -} - type SharedInboxEndpoint { id: ID! uri: String diff --git a/backend/src/schema/types/type/EMOTED.gql b/backend/src/schema/types/type/EMOTED.gql index d40eed0c4..cb8d37d62 100644 --- a/backend/src/schema/types/type/EMOTED.gql +++ b/backend/src/schema/types/type/EMOTED.gql @@ -8,3 +8,14 @@ type EMOTED @relation(name: "EMOTED") { createdAt: String updatedAt: String } + +input _EMOTEDInput { + emotion: Emotion + createdAt: String + updatedAt: String +} + +type Mutation { + AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED + RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED +} diff --git a/backend/src/schema/types/type/EmailAddress.gql b/backend/src/schema/types/type/EmailAddress.gql index 63b39d457..fe7e4cffb 100644 --- a/backend/src/schema/types/type/EmailAddress.gql +++ b/backend/src/schema/types/type/EmailAddress.gql @@ -1,6 +1,5 @@ type EmailAddress { - id: ID! - email: String! + email: ID! verifiedAt: String createdAt: String } @@ -19,5 +18,6 @@ type Mutation { avatarUpload: Upload locationName: String about: String + termsAndConditionsAgreedVersion: String ): User } diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 0e1121cd2..5b11757d3 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -52,6 +52,10 @@ type Post { @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") } +input _PostInput { + id: ID! +} + type Mutation { CreatePost( id: ID @@ -77,6 +81,7 @@ type Mutation { language: String categoryIds: [ID] ): Post + DeletePost(id: ID!): Post AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED } diff --git a/backend/src/schema/types/type/Tag.gql b/backend/src/schema/types/type/Tag.gql index c9c36343a..84c6ee7e7 100644 --- a/backend/src/schema/types/type/Tag.gql +++ b/backend/src/schema/types/type/Tag.gql @@ -1,7 +1,6 @@ type Tag { id: ID! taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN") - taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN") taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)") taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)") deleted: Boolean diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 6151d0708..1c1b9041d 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -1,173 +1,174 @@ type User { - id: ID! - actorId: String - name: String - email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email") - slug: String! - avatar: String - coverImg: String - deleted: Boolean - disabled: Boolean - disabledBy: User @relation(name: "DISABLED", direction: "IN") - role: UserGroup! - publicKey: String - invitedBy: User @relation(name: "INVITED", direction: "IN") - invited: [User] @relation(name: "INVITED", direction: "OUT") + id: ID! + actorId: String + name: String + email: String! @cypher(statement: "MATCH (this)-[: PRIMARY_EMAIL]->(e: EmailAddress) RETURN e.email") + slug: String! + avatar: String + coverImg: String + deleted: Boolean + disabled: Boolean + disabledBy: User @relation(name: "DISABLED", direction: "IN") + role: UserGroup! + publicKey: String + invitedBy: User @relation(name: "INVITED", direction: "IN") + invited: [User] @relation(name: "INVITED", direction: "OUT") - location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") - locationName: String - about: String - socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN") + location: Location @cypher(statement: "MATCH (this)-[: IS_IN]->(l: Location) RETURN l") + locationName: String + about: String + socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN") - #createdAt: DateTime - #updatedAt: DateTime - createdAt: String - updatedAt: String + # createdAt: DateTime + # updatedAt: DateTime + createdAt: String + updatedAt: String - friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") - friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") + termsAndConditionsAgreedVersion: String - following: [User]! @relation(name: "FOLLOWS", direction: "OUT") - followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)") + friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") + friendsCount: Int! @cypher(statement: "MATCH (this)<-[: FRIENDS]->(r: User) RETURN COUNT(DISTINCT r)") - followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") - followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") + following: [User]! @relation(name: "FOLLOWS", direction: "OUT") + followingCount: Int! @cypher(statement: "MATCH (this)-[: FOLLOWS]->(r: User) RETURN COUNT(DISTINCT r)") - # Is the currently logged in user following that user? - followedByCurrentUser: Boolean! @cypher( - statement: """ - MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) - RETURN COUNT(u) >= 1 - """ - ) - isBlocked: Boolean! @cypher( - statement: """ - MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) - RETURN COUNT(u) >= 1 - """ - ) + followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") + followedByCount: Int! @cypher(statement: "MATCH (this)<-[: FOLLOWS]-(r: User) RETURN COUNT(DISTINCT r)") - #contributions: [WrittenPost]! - #contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! - # @cypher( - # statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp" - # ) - contributions: [Post]! @relation(name: "WROTE", direction: "OUT") - contributionsCount: Int! @cypher( - statement: """ - MATCH (this)-[:WROTE]->(r:Post) - WHERE NOT r.deleted = true AND NOT r.disabled = true - RETURN COUNT(r) - """ - ) + # Is the currently logged in user following that user? + followedByCurrentUser: Boolean! @cypher( + statement: """ + MATCH (this)<-[: FOLLOWS]-(u: User { id: $cypherParams.currentUserId}) + RETURN COUNT(u) >= 1 + """ + ) + isBlocked: Boolean! @cypher( + statement: """ + MATCH (this)<-[: BLOCKED]-(u: User { id: $cypherParams.currentUserId}) + RETURN COUNT(u) >= 1 + """ + ) - comments: [Comment]! @relation(name: "WROTE", direction: "OUT") - commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(:Comment)-[:COMMENTS]->(p:Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))") + # contributions: [WrittenPost]! + # contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! + # @cypher( + # statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp" + # ) + contributions: [Post]! @relation(name: "WROTE", direction: "OUT") + contributionsCount: Int! @cypher( + statement: """ + MATCH (this)-[: WROTE]->(r: Post) + WHERE NOT r.deleted = true AND NOT r.disabled = true + RETURN COUNT(r) + """ + ) - shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT") - shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") + comments: [Comment]! @relation(name: "WROTE", direction: "OUT") + commentedCount: Int! @cypher(statement: "MATCH (this)-[: WROTE]->(: Comment)-[: COMMENTS]->(p: Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))") - organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT") - organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT") + shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT") + shoutedCount: Int! @cypher(statement: "MATCH (this)-[: SHOUTED]->(r: Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") - categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") + categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") - badges: [Badge]! @relation(name: "REWARDED", direction: "IN") - badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") + badges: [Badge]! @relation(name: "REWARDED", direction: "IN") + badgesCount: Int! @cypher(statement: "MATCH (this)<-[: REWARDED]-(r: Badge) RETURN COUNT(r)") - emotions: [EMOTED] + emotions: [EMOTED] } input _UserFilter { - AND: [_UserFilter!] - OR: [_UserFilter!] - name_contains: String - about_contains: String - slug_contains: String - id: ID - id_not: ID - id_in: [ID!] - id_not_in: [ID!] - id_contains: ID - id_not_contains: ID - id_starts_with: ID - id_not_starts_with: ID - id_ends_with: ID - id_not_ends_with: ID - friends: _UserFilter - friends_not: _UserFilter - friends_in: [_UserFilter!] - friends_not_in: [_UserFilter!] - friends_some: _UserFilter - friends_none: _UserFilter - friends_single: _UserFilter - friends_every: _UserFilter - following: _UserFilter - following_not: _UserFilter - following_in: [_UserFilter!] - following_not_in: [_UserFilter!] - following_some: _UserFilter - following_none: _UserFilter - following_single: _UserFilter - following_every: _UserFilter - followedBy: _UserFilter - followedBy_not: _UserFilter - followedBy_in: [_UserFilter!] - followedBy_not_in: [_UserFilter!] - followedBy_some: _UserFilter - followedBy_none: _UserFilter - followedBy_single: _UserFilter - followedBy_every: _UserFilter + AND: [_UserFilter!] + OR: [_UserFilter!] + name_contains: String + about_contains: String + slug_contains: String + id: ID + id_not: ID + id_in: [ID!] + id_not_in: [ID!] + id_contains: ID + id_not_contains: ID + id_starts_with: ID + id_not_starts_with: ID + id_ends_with: ID + id_not_ends_with: ID + friends: _UserFilter + friends_not: _UserFilter + friends_in: [_UserFilter!] + friends_not_in: [_UserFilter!] + friends_some: _UserFilter + friends_none: _UserFilter + friends_single: _UserFilter + friends_every: _UserFilter + following: _UserFilter + following_not: _UserFilter + following_in: [_UserFilter!] + following_not_in: [_UserFilter!] + following_some: _UserFilter + following_none: _UserFilter + following_single: _UserFilter + following_every: _UserFilter + followedBy: _UserFilter + followedBy_not: _UserFilter + followedBy_in: [_UserFilter!] + followedBy_not_in: [_UserFilter!] + followedBy_some: _UserFilter + followedBy_none: _UserFilter + followedBy_single: _UserFilter + followedBy_every: _UserFilter } type Query { - User( - id: ID - email: String - actorId: String - name: String - slug: String - avatar: String - coverImg: String - role: UserGroup - locationName: String - about: String - createdAt: String - updatedAt: String - friendsCount: Int - followingCount: Int - followedByCount: Int - followedByCurrentUser: Boolean - contributionsCount: Int - commentedCount: Int - shoutedCount: Int - badgesCount: Int - first: Int - offset: Int - orderBy: [_UserOrdering] - filter: _UserFilter - ): [User] + User( + id: ID + email: String + actorId: String + name: String + slug: String + avatar: String + coverImg: String + role: UserGroup + locationName: String + about: String + createdAt: String + updatedAt: String + friendsCount: Int + followingCount: Int + followedByCount: Int + followedByCurrentUser: Boolean + contributionsCount: Int + commentedCount: Int + shoutedCount: Int + badgesCount: Int + first: Int + offset: Int + orderBy: [_UserOrdering] + filter: _UserFilter + ): [User] - blockedUsers: [User] + blockedUsers: [User] + currentUser: User } type Mutation { - UpdateUser ( - id: ID! - name: String - email: String - slug: String - avatar: String - coverImg: String - avatarUpload: Upload - locationName: String - about: String - ): User + UpdateUser ( + id: ID! + name: String + email: String + slug: String + avatar: String + coverImg: String + avatarUpload: Upload + locationName: String + about: String + termsAndConditionsAgreedVersion: String + ): User - DeleteUser(id: ID!, resource: [Deletable]): User + DeleteUser(id: ID!, resource: [Deletable]): User - block(id: ID!): User - unblock(id: ID!): User + block(id: ID!): User + unblock(id: ID!): User } diff --git a/backend/src/seed/factories/users.js b/backend/src/seed/factories/users.js index af1699253..0ed1d4bc5 100644 --- a/backend/src/seed/factories/users.js +++ b/backend/src/seed/factories/users.js @@ -14,6 +14,8 @@ export default function create() { role: 'user', avatar: faker.internet.avatar(), about: faker.lorem.paragraph(), + // termsAndConditionsAgreedAt: new Date().toISOString(), + termsAndConditionsAgreedVersion: '0.0.1', } defaults.slug = slugify(defaults.name, { lower: true }) args = { diff --git a/backend/yarn.lock b/backend/yarn.lock index 2130cc364..707d8e9a9 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -681,7 +681,7 @@ pirates "^4.0.0" source-map-support "^0.5.9" -"@babel/runtime-corejs2@^7.5.5": +"@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.5.5.tgz#c3214c08ef20341af4187f1c9fbdc357fbec96b2" integrity sha512-FYATQVR00NSNi7mUfpPDp7E8RYMXDuO8gaix7u/w3GekfUinKgX1AcTxs7SoiEmoEW9mbpjrwqWSW6zCmw5h8A== @@ -5811,10 +5811,10 @@ mem@~5.1.1: mimic-fn "^2.1.0" p-is-promise "^2.1.0" -memoize-one@~5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.0.tgz#ce7af291c0e2fe041b709cac5e8c7b198c994286" - integrity sha512-p3tPVJNrjOkJ0vk0FRn6yv898qlQZct1rsQAXuwK9X5brNVajPv/y13ytrUByzSS8olyzeqCLX8BKEWmTmXa1A== +memoize-one@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== memorystream@^0.3.1: version "0.3.1" @@ -5912,14 +5912,14 @@ metascraper-publisher@^5.6.5: dependencies: "@metascraper/helpers" "^5.6.6" -metascraper-soundcloud@^5.6.5: - version "5.6.6" - resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.6.6.tgz#7f609cd1f5ad4d466d8ac5e232bb3e2c9608ec24" - integrity sha512-8p3tI9BbD3MKv37t5gsh8E5ZDMZvZLKbLqK6hKEG2v0I0fzmpDQdUwhgo6NI4eAmOrPsyiPeQmu6sZLTWeK3OA== +metascraper-soundcloud@^5.6.7: + version "5.6.7" + resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.6.7.tgz#c06a5398c85ec2d8e139c7f2016451ebf54de039" + integrity sha512-pHfhrAej8adCwK5Vo0IIOvoCTSWM1gSXeUQAwSyPGySME7NRMfgMq/TJSFF24Qih0UrE7u/4ogtGR3k3/Sno6g== dependencies: "@metascraper/helpers" "^5.6.6" - memoize-one "~5.1.0" - tldts "~5.3.2" + memoize-one "~5.1.1" + tldts "~5.4.0" metascraper-title@^5.6.5: version "5.6.6" @@ -5944,15 +5944,15 @@ metascraper-video@^5.6.5: "@metascraper/helpers" "^5.6.6" lodash "~4.17.15" -metascraper-youtube@^5.6.3: - version "5.6.6" - resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.6.6.tgz#ba098f1c92306e39645a11ace263c24d77223a18" - integrity sha512-xV9uyMNUdXhsNMqnFk7bde2ygt2rKHJ8r+3Ft+GbgP+Zdw6PXOCy6/+yA/kF5nZouF7DOBBTw2bWEXqu08eCmQ== +metascraper-youtube@^5.6.7: + version "5.6.7" + resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.6.7.tgz#48c68aefb8b67aaae1df532dad8b4c0d6e8b7240" + integrity sha512-8/v+PwkNyVgRrD5SVVXH7aY1ld0NVlHlz/RhX6sre9OoYY70axa+zNttEp15zGcbOpS+g3RtEb7So/fbyb/Xcg== dependencies: "@metascraper/helpers" "^5.6.6" get-video-id "~3.1.4" is-reachable "~3.1.0" - memoize-one "~5.1.0" + memoize-one "~5.1.1" p-locate "~4.1.0" metascraper@^4.10.3: @@ -8154,17 +8154,17 @@ tlds@^1.187.0, tlds@^1.203.0: resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.203.1.tgz#4dc9b02f53de3315bc98b80665e13de3edfc1dfc" integrity sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw== -tldts-core@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.3.2.tgz#3823653310c2dc6e51d00998098a72a3cc203ea9" - integrity sha512-bGI7MvrFXnbNhSkKEKAjiRo+eoIuIsuzF/hOtpI7HLMLWamIGtrLuNv2nlBOdN/h6iB6B32B4MxmwPWTZ70TaQ== +tldts-core@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.4.0.tgz#bd69ca3ad96a78ab675c74684c6d0717929777ec" + integrity sha512-kfDnB7fcNgNUcn5k21TPM/MbXqJNt2bBGQRfGyE39H334Qk+qNcSqw9It3YPxvrA7msl7DQ8wvcIsa0y55auug== -tldts@~5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.3.2.tgz#d7e9b31b70b7d7687a16fe31a18ec2ca8a33f852" - integrity sha512-R96Q0sJoD1vMYZ5YI0om32AZW2SYDxWfcg0111gnJYIvfyVgHOwyYj2dLFhpkPCSH7AwwUe0MmewmUrQhJXRjQ== +tldts@~5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.4.0.tgz#ded769383615341660243acf547f197eb48f6795" + integrity sha512-S1CmstJxRb6KK+uLHhMUXXkI/zjA20RGM9QYkLgDEj42C9Zmr+OLjzEqf4Id/EWppuLi1z9FdNsz8/qi/pLCGA== dependencies: - tldts-core "^5.3.2" + tldts-core "^5.4.0" tmp@^0.0.33: version "0.0.33" @@ -8862,6 +8862,13 @@ xmldom@0.1.19: resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc" integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw= +xregexp@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.2.4.tgz#02a4aea056d65a42632c02f0233eab8e4d7e57ed" + integrity sha512-sO0bYdYeJAJBcJA8g7MJJX7UrOZIfJPd8U2SC7B2Dd/J24U0aQNoGp33shCaBSWeb0rD5rh6VBUIXOkGal1TZA== + dependencies: + "@babel/runtime-corejs2" "^7.2.0" + xtend@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" diff --git a/cypress.env.template.json b/cypress.env.template.json index bd03f6381..f5916bb89 100644 --- a/cypress.env.template.json +++ b/cypress.env.template.json @@ -3,4 +3,4 @@ "NEO4J_URI": "bolt://localhost:7687", "NEO4J_USERNAME": "neo4j", "NEO4J_PASSWORD": "letmein" -} +} \ No newline at end of file diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js index 180353328..fb395d361 100644 --- a/cypress/integration/common/report.js +++ b/cypress/integration/common/report.js @@ -31,6 +31,7 @@ Given('I am logged in with a {string} role', role => { cy.factory().create('User', { email: `${role}@example.org`, password: '1234', + termsAndConditionsAgreedVersion: "0.0.2", role }) cy.login({ diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index f7ab18707..d712ee9b1 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -1,4 +1,8 @@ -import { Given, When, Then } from "cypress-cucumber-preprocessor/steps"; +import { + Given, + When, + Then +} from "cypress-cucumber-preprocessor/steps"; import helpers from "../../support/helpers"; /* global cy */ @@ -9,12 +13,16 @@ let loginCredentials = { email: "peterpan@example.org", password: "1234" }; +const termsAndConditionsAgreedVersion = { + termsAndConditionsAgreedVersion: "0.0.2" +}; const narratorParams = { id: 'id-of-peter-pan', name: "Peter Pan", slug: "peter-pan", avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg", - ...loginCredentials + ...loginCredentials, + ...termsAndConditionsAgreedVersion, }; Given("I am logged in", () => { @@ -28,25 +36,54 @@ Given("we have a selection of categories", () => { Given("we have a selection of tags and categories as well as posts", () => { cy.createCategories("cat12") .factory() - .create("Tag", { id: "Ecology" }) - .create("Tag", { id: "Nature" }) - .create("Tag", { id: "Democracy" }); + .create("Tag", { + id: "Ecology" + }) + .create("Tag", { + id: "Nature" + }) + .create("Tag", { + id: "Democracy" + }); cy.factory() - .create("User", { id: 'a1' }) - .create("Post", {authorId: 'a1', tagIds: [ "Ecology", "Nature", "Democracy" ], categoryIds: ["cat12"] }) - .create("Post", {authorId: 'a1', tagIds: [ "Nature", "Democracy" ], categoryIds: ["cat121"] }); + .create("User", { + id: 'a1' + }) + .create("Post", { + authorId: 'a1', + tagIds: ["Ecology", "Nature", "Democracy"], + categoryIds: ["cat12"] + }) + .create("Post", { + authorId: 'a1', + tagIds: ["Nature", "Democracy"], + categoryIds: ["cat121"] + }); cy.factory() - .create("User", { id: 'a2'}) - .create("Post", { authorId: 'a2', tagIds: ['Nature', 'Democracy'], categoryIds: ["cat12"] }); + .create("User", { + id: 'a2' + }) + .create("Post", { + authorId: 'a2', + tagIds: ['Nature', 'Democracy'], + categoryIds: ["cat12"] + }); cy.factory() - .create("Post", { authorId: narratorParams.id, tagIds: ['Democracy'], categoryIds: ["cat122"] }) + .create("Post", { + authorId: narratorParams.id, + tagIds: ['Democracy'], + categoryIds: ["cat122"] + }) }); Given("we have the following user accounts:", table => { table.hashes().forEach(params => { - cy.factory().create("User", params); + cy.factory().create("User", { + ...params, + ...termsAndConditionsAgreedVersion + }); }); }); @@ -57,7 +94,8 @@ Given("I have a user account", () => { Given("my user account has the role {string}", role => { cy.factory().create("User", { role, - ...loginCredentials + ...loginCredentials, + ...termsAndConditionsAgreedVersion, }); }); @@ -117,7 +155,9 @@ Given("I previously switched the language to {string}", name => { }); Then("the whole user interface appears in {string}", name => { - const { code } = helpers.getLangByName(name); + const { + code + } = helpers.getLangByName(name); cy.get(`html[lang=${code}]`); cy.getCookie("locale").should("have.property", "value", code); }); @@ -151,7 +191,9 @@ Given("we have the following posts in our database:", table => { icon: "smile" }) - table.hashes().forEach(({ ...postAttributes }, i) => { + table.hashes().forEach(({ + ...postAttributes + }, i) => { postAttributes = { ...postAttributes, deleted: Boolean(postAttributes.deleted), @@ -229,10 +271,15 @@ Then("the first post on the landing page has the title:", title => { Then( "the page {string} returns a 404 error with a message:", (route, message) => { - cy.request({ url: route, failOnStatusCode: false }) + cy.request({ + url: route, + failOnStatusCode: false + }) .its("status") .should("eq", 404); - cy.visit(route, { failOnStatusCode: false }); + cy.visit(route, { + failOnStatusCode: false + }); cy.get(".error").should("contain", message); } ); @@ -240,7 +287,10 @@ Then( Given("my user account has the following login credentials:", table => { loginCredentials = table.hashes()[0]; cy.debug(); - cy.factory().create("User", loginCredentials); + cy.factory().create("User", { + ...termsAndConditionsAgreedVersion, + ...loginCredentials + }); }); When("I fill the password form with:", table => { @@ -259,7 +309,9 @@ When("submit the form", () => { Then("I cannot login anymore with password {string}", password => { cy.reload(); - const { email } = loginCredentials; + const { + email + } = loginCredentials; cy.visit(`/login`); cy.get("input[name=email]") .trigger("focus") @@ -280,14 +332,22 @@ Then("I can login successfully with password {string}", password => { cy.reload(); cy.login({ ...loginCredentials, - ...{ password } + ...{ + password + } }); cy.get(".iziToast-wrapper").should("contain", "You are logged in!"); }); When("I log in with the following credentials:", table => { - const { email, password } = table.hashes()[0]; - cy.login({ email, password }); + const { + email, + password + } = table.hashes()[0]; + cy.login({ + email, + password + }); }); When("open the notification menu and click on the first item", () => { @@ -337,12 +397,14 @@ Then("there are no notifications in the top menu", () => { Given("there is an annoying user called {string}", name => { const annoyingParams = { email: "spammy-spammer@example.org", - password: "1234" + password: "1234", + ...termsAndConditionsAgreedVersion }; cy.factory().create("User", { ...annoyingParams, id: "annoying-user", - name + name, + ...termsAndConditionsAgreedVersion, }); }); @@ -364,7 +426,9 @@ When( cy.get(".user-content-menu .content-menu-trigger").click(); cy.get(".popover .ds-menu-item-link") .contains(button) - .click({ force: true }); + .click({ + force: true + }); } ); @@ -379,10 +443,14 @@ When("I navigate to my {string} settings page", settingsPage => { Given("I follow the user {string}", name => { cy.neode() - .first("User", { name }) + .first("User", { + name + }) .then(followed => { cy.neode() - .first("User", { name: narratorParams.name }) + .first("User", { + name: narratorParams.name + }) .relateTo(followed, "following"); }); }); @@ -390,7 +458,11 @@ Given("I follow the user {string}", name => { Given('"Spammy Spammer" wrote a post {string}', title => { cy.createCategories("cat21") .factory() - .create("Post", { authorId: 'annoying-user', title, categoryIds: ["cat21"] }); + .create("Post", { + authorId: 'annoying-user', + title, + categoryIds: ["cat21"] + }); }); Then("the list of posts of this user is empty", () => { @@ -409,23 +481,37 @@ Then("nobody is following the user profile anymore", () => { Given("I wrote a post {string}", title => { cy.createCategories(`cat213`, title) .factory() - .create("Post", { authorId: narratorParams.id, title, categoryIds: ["cat213"] }); + .create("Post", { + authorId: narratorParams.id, + title, + categoryIds: ["cat213"] + }); }); When("I block the user {string}", name => { cy.neode() - .first("User", { name }) + .first("User", { + name + }) .then(blocked => { cy.neode() - .first("User", { name: narratorParams.name }) + .first("User", { + name: narratorParams.name + }) .relateTo(blocked, "blocked"); }); }); When("I log in with:", table => { const [firstRow] = table.hashes(); - const { Email, Password } = firstRow; - cy.login({ email: Email, password: Password }); + const { + Email, + Password + } = firstRow; + cy.login({ + email: Email, + password: Password + }); }); Then("I see only one post with the title {string}", title => { @@ -433,4 +519,4 @@ Then("I see only one post with the title {string}", title => { .find(".post-link") .should("have.length", 1); cy.get(".main-container").contains(".post-link", title); -}); +}); \ No newline at end of file diff --git a/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh b/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh index 71ed3b58b..f9334ccd2 100755 --- a/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh +++ b/deployment/legacy-migration/maintenance-worker/migration/mongo/export.sh @@ -33,20 +33,20 @@ export_collection "categories" export_collection "comments" export_collection_query "contributions" '{"type": "DELETED"}' "DELETED" export_collection_query "contributions" '{"type": "post"}' "post" -export_collection_query "contributions" '{"type": "cando"}' "cando" +# export_collection_query "contributions" '{"type": "cando"}' "cando" export_collection "emotions" -export_collection_query "follows" '{"foreignService": "organizations"}' "organizations" +# export_collection_query "follows" '{"foreignService": "organizations"}' "organizations" export_collection_query "follows" '{"foreignService": "users"}' "users" -export_collection "invites" -export_collection "organizations" -export_collection "pages" -export_collection "projects" -export_collection "settings" +# export_collection "invites" +# export_collection "organizations" +# export_collection "pages" +# export_collection "projects" +# export_collection "settings" export_collection "shouts" -export_collection "status" +# export_collection "status" export_collection "users" -export_collection "userscandos" -export_collection "usersettings" +# export_collection "userscandos" +# export_collection "usersettings" # Close SSH Tunnel ssh -S my-ctrl-socket -O check -l ${SSH_USERNAME} ${SSH_HOST} diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql index 6fad4218d..f09b5ad71 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql @@ -125,7 +125,6 @@ [ ] wasSeeded: { type: Boolean } } */ - CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as post MERGE (p:Post {id: post._id["$oid"]}) ON CREATE SET @@ -148,6 +147,10 @@ MATCH (c:Category {id: categoryId}) MERGE (p)-[:CATEGORIZED]->(c) WITH p, post.tags AS tags UNWIND tags AS tag -MERGE (t:Tag {id: apoc.text.clean(tag), disabled: false, deleted: false}) +WITH apoc.text.replace(tag, '[^\\p{L}0-9]', '') as tagNoSpacesAllowed +CALL apoc.when(tagNoSpacesAllowed =~ '^((\\p{L}+[\\p{L}0-9]*)|([0-9]+\\p{L}+[\\p{L}0-9]*))$', 'RETURN tagNoSpacesAllowed', '', {tagNoSpacesAllowed: tagNoSpacesAllowed}) +YIELD value as validated +WHERE validated.tagNoSpacesAllowed IS NOT NULL +MERGE (t:Tag { id: validated.tagNoSpacesAllowed, disabled: false, deleted: false }) MERGE (p)-[:TAGGED]->(t) ; diff --git a/neo4j/db_setup.sh b/neo4j/db_setup.sh index 51276cf39..ba90ee5f4 100755 --- a/neo4j/db_setup.sh +++ b/neo4j/db_setup.sh @@ -26,13 +26,11 @@ CREATE CONSTRAINT ON (p:Post) ASSERT p.id IS UNIQUE; CREATE CONSTRAINT ON (c:Comment) ASSERT c.id IS UNIQUE; CREATE CONSTRAINT ON (c:Category) ASSERT c.id IS UNIQUE; CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE; -CREATE CONSTRAINT ON (o:Organization) ASSERT o.id IS UNIQUE; CREATE CONSTRAINT ON (t:Tag) ASSERT t.id IS UNIQUE; CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE; CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE; CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE; -CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE; CREATE CONSTRAINT ON (e:EmailAddress) ASSERT e.email IS UNIQUE; ' | cypher-shell diff --git a/webapp/components/Editor/Editor.vue b/webapp/components/Editor/Editor.vue index 27a94e6b6..75f550c2a 100644 --- a/webapp/components/Editor/Editor.vue +++ b/webapp/components/Editor/Editor.vue @@ -26,6 +26,7 @@ import { Editor, EditorContent } from 'tiptap' import { History } from 'tiptap-extensions' import linkify from 'linkify-it' import stringHash from 'string-hash' +import { replace, build } from 'xregexp/xregexp-all.js' import * as key from '../../constants/keycodes' import { HASHTAG, MENTION } from '../../constants/editor' @@ -214,8 +215,9 @@ export default { }, sanitizeQuery(query) { if (this.suggestionType === HASHTAG) { - // remove all not allowed chars - query = query.replace(/[^a-zA-Z0-9]/gm, '') + // remove all non unicode letters and non digits + const regexMatchAllNonUnicodeLettersOrDigits = build('[^\\pL0-9]') + query = replace(query, regexMatchAllNonUnicodeLettersOrDigits, '', 'all') // if the query is only made of digits, make it empty return query.replace(/[0-9]/gm, '') === '' ? '' : query } diff --git a/webapp/components/Editor/SuggestionList.vue b/webapp/components/Editor/SuggestionList.vue index b351e6b74..3d480d187 100644 --- a/webapp/components/Editor/SuggestionList.vue +++ b/webapp/components/Editor/SuggestionList.vue @@ -7,15 +7,15 @@ :class="{ 'is-selected': navigatedItemIndex === index }" @click="selectItem(item)" > - {{ createItemLabel(item) }} + {{ createItemLabel(item) | truncate(50) }}