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/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/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..93076e334 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)
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/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/User.gql b/backend/src/schema/types/type/User.gql
index 6151d0708..83d5343b2 100644
--- a/backend/src/schema/types/type/User.gql
+++ b/backend/src/schema/types/type/User.gql
@@ -1,173 +1,177 @@
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")
+ organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
+ organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
- badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
- badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
+ categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
- emotions: [EMOTED]
+ badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
+ badgesCount: Int! @cypher(statement: "MATCH (this)<-[: REWARDED]-(r: Badge) RETURN COUNT(r)")
+
+ 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
+}
\ No newline at end of file
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/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/webapp/components/Registration/CreateUserAccount.spec.js b/webapp/components/Registration/CreateUserAccount.spec.js
index a8d63193f..3ee358db6 100644
--- a/webapp/components/Registration/CreateUserAccount.spec.js
+++ b/webapp/components/Registration/CreateUserAccount.spec.js
@@ -77,6 +77,7 @@ describe('CreateUserAccount', () => {
email: 'sixseven@example.org',
nonce: '666777',
password: 'hellopassword',
+ termsAndConditionsAgreedVersion: '0.0.2',
},
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
diff --git a/webapp/components/Registration/CreateUserAccount.vue b/webapp/components/Registration/CreateUserAccount.vue
index e5b1ee38f..e1ff98a5c 100644
--- a/webapp/components/Registration/CreateUserAccount.vue
+++ b/webapp/components/Registration/CreateUserAccount.vue
@@ -55,7 +55,10 @@
v-model="termsAndConditionsConfirmed"
:checked="termsAndConditionsConfirmed"
/>
-
+
@@ -84,9 +87,24 @@ import gql from 'graphql-tag'
import PasswordStrength from '../Password/Strength'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper'
+import { VERSION } from '~/constants/terms-and-conditions-version.js'
+
+/* TODO: hier muss die version rein */
export const SignupVerificationMutation = gql`
- mutation($nonce: String!, $name: String!, $email: String!, $password: String!) {
- SignupVerification(nonce: $nonce, email: $email, name: $name, password: $password) {
+ mutation(
+ $nonce: String!
+ $name: String!
+ $email: String!
+ $password: String!
+ $termsAndConditionsAgreedVersion: String!
+ ) {
+ SignupVerification(
+ nonce: $nonce
+ email: $email
+ name: $name
+ password: $password
+ termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
+ ) {
id
name
slug
@@ -135,10 +153,11 @@ export default {
async submit() {
const { name, password, about } = this.formData
const { email, nonce } = this
+ const termsAndConditionsAgreedVersion = VERSION
try {
await this.$apollo.mutate({
mutation: SignupVerificationMutation,
- variables: { name, password, about, email, nonce },
+ variables: { name, password, about, email, nonce, termsAndConditionsAgreedVersion },
})
this.success = true
setTimeout(() => {
diff --git a/webapp/constants/terms-and-conditions-version.js b/webapp/constants/terms-and-conditions-version.js
new file mode 100644
index 000000000..e8f3c9298
--- /dev/null
+++ b/webapp/constants/terms-and-conditions-version.js
@@ -0,0 +1 @@
+export const VERSION = '0.0.2'
diff --git a/webapp/layouts/default.vue b/webapp/layouts/default.vue
index c9d21e704..b136a5a71 100644
--- a/webapp/layouts/default.vue
+++ b/webapp/layouts/default.vue
@@ -224,7 +224,7 @@ export default {
},
showFilterPostsDropdown() {
const [firstRoute] = this.$route.matched
- return firstRoute.name === 'index'
+ return firstRoute && firstRoute.name === 'index'
},
},
watch: {
diff --git a/webapp/locales/de.json b/webapp/locales/de.json
index aa8ff433e..70c66ae0b 100644
--- a/webapp/locales/de.json
+++ b/webapp/locales/de.json
@@ -21,6 +21,7 @@
}
},
"site": {
+ "thanks": "Danke!",
"made": "Mit ❤ gemacht",
"imprint": "Impressum",
"data-privacy": "Datenschutz",
@@ -34,8 +35,7 @@
"responsible": "Verantwortlicher gemäß § 55 Abs. 2 RStV ",
"bank": "Bankverbindung",
"germany": "Deutschland",
- "code-of-conduct": "Verhaltenscodex",
- "termsAndConditionsConfirmed": "Ich habe die Nutzungsbedingungen durchgelesen und stimme ihnen zu."
+ "code-of-conduct": "Verhaltenscodex"
},
"sorting": {
"newest": "Neuste",
@@ -575,6 +575,11 @@
"get-help": "Wenn du einem inakzeptablen Verhalten ausgesetzt bist, es miterlebst oder andere Bedenken hast, benachrichtige bitte so schnell wie möglich einen Organisator der Gemeinschaft und verlinke oder verweise auf den entsprechenden Inhalt:"
},
"termsAndConditions": {
+ "termsAndConditionsConfirmed": "Ich habe die Nutzungsbedingungen durchgelesen und stimme ihnen zu.",
+ "newTermsAndConditions": "Neue Nutzungsbedingungen",
+ "termsAndConditionsNewConfirmText": "Bitte lies dir die neue Nutzungsbedingungen jetzt durch!",
+ "termsAndConditionsNewConfirm": "Ich habe die neuen Nutzungsbedingungen durchgelesen und stimme zu.",
+ "agree": "Ich stimme zu!",
"risk": {
"title": "Unfallgefahr",
"description": "Das ist eine Testversion! Alle Daten, Dein Profil und die Server können jederzeit komplett vernichtet, verloren, verbrannt und vielleicht auch in der Nähe von Alpha Centauri synchronisiert werden. Die Benutzung läuft auf eigene Gefahr. Mit kommerziellen Nebenwirkungen ist jedoch nicht zu rechnen."
@@ -610,4 +615,4 @@
"have-fun": "Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎",
"closing": "Herzlichst
Euer Human Connection Team"
}
-}
+}
\ No newline at end of file
diff --git a/webapp/locales/en.json b/webapp/locales/en.json
index f0053f1a1..ecbc7da33 100644
--- a/webapp/locales/en.json
+++ b/webapp/locales/en.json
@@ -21,6 +21,7 @@
}
},
"site": {
+ "thanks": "Thanks!",
"made": "Made with ❤",
"imprint": "Imprint",
"termsAndConditions": "Terms and conditions",
@@ -34,8 +35,7 @@
"responsible": "responsible for contents of this page (§ 55 Abs. 2 RStV)",
"bank": "bank account",
"germany": "Germany",
- "code-of-conduct": "Code of Conduct",
- "termsAndConditionsConfirmed": "I have read and confirmed the terms and conditions."
+ "code-of-conduct": "Code of Conduct"
},
"sorting": {
"newest": "Newest",
@@ -575,6 +575,11 @@
"get-help": "If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible and link or refer to the corresponding content:"
},
"termsAndConditions": {
+ "newTermsAndConditions": "New Terms and Conditions",
+ "termsAndConditionsConfirmed": "I have read and confirmed the Terms and Conditions.",
+ "termsAndConditionsNewConfirmText": "Please read the new terms of use now!",
+ "termsAndConditionsNewConfirm": "I have read and agree to the new terms of conditions.",
+ "agree": "I agree!",
"risk": {
"title": "Risk of accident",
"description": "This is a test version! All data, your profile and the server can be completely destroyed, wiped out, lost, burnt and eventually synchronised near Alpha Centauri at any time. Use on your own risk. Commercial effects are not likely though."
@@ -610,4 +615,4 @@
"have-fun": "Now have fun with the alpha version of Human Connection! For the first universal peace. ♥︎",
"closing": "Thank you very much
your Human Connection Team"
}
-}
+}
\ No newline at end of file
diff --git a/webapp/middleware/termsAndConditions.js b/webapp/middleware/termsAndConditions.js
new file mode 100644
index 000000000..64141eed0
--- /dev/null
+++ b/webapp/middleware/termsAndConditions.js
@@ -0,0 +1,19 @@
+import isEmpty from 'lodash/isEmpty'
+
+export default async ({ store, env, route, redirect }) => {
+ let publicPages = env.publicPages
+ // only affect non public pages
+ if (publicPages.indexOf(route.name) >= 0) {
+ return true
+ }
+ if (route.name === 'terms-and-conditions-confirm') return true // avoid endless loop
+
+ if (store.getters['auth/termsAndConditionsAgreed']) return true
+
+ let params = {}
+ if (!isEmpty(route.path) && route.path !== '/') {
+ params.path = route.path
+ }
+
+ return redirect('/terms-and-conditions-confirm', params)
+}
diff --git a/webapp/nuxt.config.js b/webapp/nuxt.config.js
index a95cac136..9a0839058 100644
--- a/webapp/nuxt.config.js
+++ b/webapp/nuxt.config.js
@@ -122,7 +122,7 @@ module.exports = {
],
router: {
- middleware: ['authenticated'],
+ middleware: ['authenticated', 'termsAndConditions'],
linkActiveClass: 'router-link-active',
linkExactActiveClass: 'router-link-exact-active',
scrollBehavior: (to, _from, savedPosition) => {
diff --git a/webapp/pages/login.vue b/webapp/pages/login.vue
index 051e32e03..65b1eb49c 100644
--- a/webapp/pages/login.vue
+++ b/webapp/pages/login.vue
@@ -75,6 +75,7 @@
diff --git a/webapp/store/auth.js b/webapp/store/auth.js
index 41e1b4164..498477660 100644
--- a/webapp/store/auth.js
+++ b/webapp/store/auth.js
@@ -1,4 +1,5 @@
import gql from 'graphql-tag'
+import { VERSION } from '~/constants/terms-and-conditions-version.js'
export const state = () => {
return {
@@ -42,6 +43,9 @@ export const getters = {
token(state) {
return state.token
},
+ termsAndConditionsAgreed(state) {
+ return state.user && state.user.termsAndConditionsAgreedVersion === VERSION
+ },
}
export const actions = {
@@ -82,6 +86,7 @@ export const actions = {
locationName
contributionsCount
commentedCount
+ termsAndConditionsAgreedVersion
socialMedia {
id
url