Merge pull request #1334 from Human-Connection/C-1187-terms-and-conditions-confirmed-function

Check if user has agreed to the current terms and conditions
This commit is contained in:
mattwr18 2019-09-04 20:55:09 +02:00 committed by GitHub
commit 4ca84ee04a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 563 additions and 227 deletions

View File

@ -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 })
}

View File

@ -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',

View File

@ -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(

View File

@ -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', () => {

View File

@ -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: {

View File

@ -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)

View File

@ -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!')
})
})
})

View File

@ -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
}

View File

@ -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
}

View File

@ -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 = {

View File

@ -3,4 +3,4 @@
"NEO4J_URI": "bolt://localhost:7687",
"NEO4J_USERNAME": "neo4j",
"NEO4J_PASSWORD": "letmein"
}
}

View File

@ -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({

View File

@ -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);
});
});

View File

@ -77,6 +77,7 @@ describe('CreateUserAccount', () => {
email: 'sixseven@example.org',
nonce: '666777',
password: 'hellopassword',
termsAndConditionsAgreedVersion: '0.0.2',
},
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)

View File

@ -55,7 +55,10 @@
v-model="termsAndConditionsConfirmed"
:checked="termsAndConditionsConfirmed"
/>
<label for="checkbox" v-html="$t('site.termsAndConditionsConfirmed')"></label>
<label
for="checkbox"
v-html="$t('termsAndConditions.termsAndConditionsConfirmed')"
></label>
</ds-text>
<template slot="footer">
@ -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(() => {

View File

@ -0,0 +1 @@
export const VERSION = '0.0.2'

View File

@ -224,7 +224,7 @@ export default {
},
showFilterPostsDropdown() {
const [firstRoute] = this.$route.matched
return firstRoute.name === 'index'
return firstRoute && firstRoute.name === 'index'
},
},
watch: {

View File

@ -21,6 +21,7 @@
}
},
"site": {
"thanks": "Danke!",
"made": "Mit &#10084; 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 <a href=\"/terms-and-conditions\" target=\"_blank\">Nutzungsbedingungen</a> 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 <a href=\"/terms-and-conditions\" target=\"_blank\">Nutzungsbedingungen</a> 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 <br><br> Euer Human Connection Team"
}
}
}

View File

@ -21,6 +21,7 @@
}
},
"site": {
"thanks": "Thanks!",
"made": "Made with &#10084;",
"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 <a href=\"/terms-and-conditions\" target=\"_blank\">terms and conditions</a>."
"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 <a href=\"/terms-and-conditions\" target=\"_blank\">Terms and Conditions</a>.",
"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 <br> <br> your Human Connection Team"
}
}
}

View File

@ -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)
}

View File

@ -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) => {

View File

@ -75,6 +75,7 @@
<script>
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import { VERSION } from '~/constants/terms-and-conditions-version.js'
export default {
components: {
@ -96,7 +97,7 @@ export default {
},
},
asyncData({ store, redirect }) {
if (store.getters['auth/isLoggedIn']) {
if (store.getters['auth/user'].termsAndConditionsAgreedVersion === VERSION) {
redirect('/')
}
},

View File

@ -0,0 +1,98 @@
<template>
<ds-container width="medium">
<ds-card icon="balance-scale" :header="$t(`termsAndConditions.newTermsAndConditions`)" centered>
<p>
<ds-button>
<nuxt-link class="post-link" :to="{ name: 'terms-and-conditions' }" target="_blank">
{{ $t(`termsAndConditions.termsAndConditionsNewConfirmText`) }}
</nuxt-link>
</ds-button>
</p>
<ds-text>
<input id="checkbox" type="checkbox" v-model="checked" :checked="checked" />
<label
for="checkbox"
v-html="$t('termsAndConditions.termsAndConditionsNewConfirm')"
></label>
</ds-text>
<template slot="footer">
<ds-button primary @click="submit" :disabled="!checked">{{ $t(`actions.save`) }}</ds-button>
</template>
</ds-card>
</ds-container>
</template>
<script>
import gql from 'graphql-tag'
import { mapGetters, mapMutations } from 'vuex'
import { VERSION } from '~/constants/terms-and-conditions-version.js'
const mutation = gql`
mutation($id: ID!, $termsAndConditionsAgreedVersion: String) {
UpdateUser(id: $id, termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion) {
id
termsAndConditionsAgreedVersion
}
}
`
export default {
layout: 'default',
head() {
return {
title: this.$t('termsAndConditions.newTermsAndConditions'),
}
},
computed: {
...mapGetters({
currentUser: 'auth/user',
}),
},
data() {
return {
checked: false,
sections: [
'risk',
'data-privacy',
'work-in-progress',
'code-of-conduct',
'moderation',
'fairness',
'questions',
'human-connection',
],
}
},
asyncData({ store, redirect }) {
if (store.getters['auth/termsAndConditionsAgreed']) {
redirect('/')
}
},
methods: {
...mapMutations({
setCurrentUser: 'auth/SET_USER',
}),
async submit() {
try {
await this.$apollo.mutate({
mutation,
variables: {
id: this.currentUser.id,
termsAndConditionsAgreedVersion: VERSION,
},
update: (store, { data: { UpdateUser } }) => {
const { termsAndConditionsAgreedVersion } = UpdateUser
this.setCurrentUser({
...this.currentUser,
termsAndConditionsAgreedVersion,
})
},
})
this.$toast.success(this.$t('site.thanks'))
this.$router.replace(this.$route.query.path || '/')
} catch (err) {
this.$toast.error(err.message)
}
},
},
}
</script>

View File

@ -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