Merge branch 'master' of github.com:Human-Connection/Human-Connection into update-legacy-migration-config

This commit is contained in:
mattwr18 2019-09-05 08:11:01 +02:00
commit 60659626c8
49 changed files with 697 additions and 533 deletions

View File

@ -83,11 +83,11 @@
"metascraper-lang-detector": "^4.8.5", "metascraper-lang-detector": "^4.8.5",
"metascraper-logo": "^5.6.5", "metascraper-logo": "^5.6.5",
"metascraper-publisher": "^5.6.5", "metascraper-publisher": "^5.6.5",
"metascraper-soundcloud": "^5.6.5", "metascraper-soundcloud": "^5.6.7",
"metascraper-title": "^5.6.5", "metascraper-title": "^5.6.5",
"metascraper-url": "^5.6.5", "metascraper-url": "^5.6.5",
"metascraper-video": "^5.6.5", "metascraper-video": "^5.6.5",
"metascraper-youtube": "^5.6.3", "metascraper-youtube": "^5.6.7",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"neo4j-driver": "~1.7.6", "neo4j-driver": "~1.7.6",
"neo4j-graphql-js": "^2.7.2", "neo4j-graphql-js": "^2.7.2",
@ -100,6 +100,7 @@
"slug": "~1.1.0", "slug": "~1.1.0",
"trunc-html": "~1.1.2", "trunc-html": "~1.1.2",
"uuid": "~3.3.3", "uuid": "~3.3.3",
"xregexp": "^4.2.4",
"wait-on": "~3.3.0" "wait-on": "~3.3.0"
}, },
"devDependencies": { "devDependencies": {
@ -130,4 +131,4 @@
"prettier": "~1.18.2", "prettier": "~1.18.2",
"supertest": "~4.0.2" "supertest": "~4.0.2"
} }
} }

View File

@ -11,10 +11,8 @@ export default {
Mutation: { Mutation: {
CreatePost: setCreatedAt, CreatePost: setCreatedAt,
CreateComment: setCreatedAt, CreateComment: setCreatedAt,
CreateOrganization: setCreatedAt,
UpdateUser: setUpdatedAt, UpdateUser: setUpdatedAt,
UpdatePost: setUpdatedAt, UpdatePost: setUpdatedAt,
UpdateComment: setUpdatedAt, UpdateComment: setUpdatedAt,
UpdateOrganization: setUpdatedAt,
}, },
} }

View File

@ -22,15 +22,5 @@ export default {
const result = await resolve(root, args, context, info) const result = await resolve(root, args, context, info)
return result 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
},
}, },
} }

View File

@ -1,17 +1,18 @@
import cheerio from 'cheerio' import cheerio from 'cheerio'
import { exec, build } from 'xregexp/xregexp-all.js'
// formats of a Hashtag: // formats of a Hashtag:
// https://en.wikipedia.org/w/index.php?title=Hashtag&oldid=905141980#Style // https://en.wikipedia.org/w/index.php?title=Hashtag&oldid=905141980#Style
// here: // here:
// 0. Search for whole string. // 0. Search for whole string.
// 1. Hashtag has only 'a-z', 'A-Z', and '0-9'. // 1. Hashtag has only all unicode characters and '0-9'.
// 2. If it starts with a digit '0-9' than 'a-z', or 'A-Z' has to follow. // 2. If it starts with a digit '0-9' than a unicode character 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 const regX = build('^/search/hashtag/((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$')
export default function(content) { export default function(content) {
if (!content) return [] if (!content) return []
const $ = cheerio.load(content) const $ = cheerio.load(content)
// We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware. // 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') const urls = $('a')
.map((_, el) => { .map((_, el) => {
return $(el).attr('href') return $(el).attr('href')
@ -19,8 +20,8 @@ export default function(content) {
.get() .get()
const hashtags = [] const hashtags = []
urls.forEach(url => { urls.forEach(url => {
let match const match = exec(url, regX)
while ((match = ID_REGEX.exec(url)) != null) { if (match != null) {
hashtags.push(match[1]) hashtags.push(match[1])
} }
}) })

View File

@ -28,9 +28,14 @@ describe('extractHashtags', () => {
}) })
it('ignores Hashtag links with not allowed character combinations', () => { 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 = const content =
'<p>Something inspirational about <a href="/search/hashtag/AbcDefXyz0123456789!*(),2" class="hashtag" target="_blank">#AbcDefXyz0123456789!*(),2</a>, <a href="/search/hashtag/0123456789" class="hashtag" target="_blank">#0123456789</a>, <a href="/search/hashtag/0123456789a" class="hashtag" target="_blank">#0123456789a</a> and <a href="/search/hashtag/AbcDefXyz0123456789" target="_blank">#AbcDefXyz0123456789</a>.</p>' '<p>Something inspirational about <a href="/search/hashtag/AbcDefXyz0123456789!*(),2" class="hashtag" target="_blank">#AbcDefXyz0123456789!*(),2</a>, <a href="/search/hashtag/0123456789" class="hashtag" target="_blank">#0123456789</a>, <a href="/search/hashtag/0123456789a" class="hashtag" target="_blank">#0123456789a</a>, <a href="/search/hashtag/AbcDefXyz0123456789" target="_blank">#AbcDefXyz0123456789</a>, and <a href="/search/hashtag/λαπ" target="_blank">#λαπ</a>.</p>'
expect(extractHashtags(content)).toEqual(['0123456789a', 'AbcDefXyz0123456789']) expect(extractHashtags(content).sort()).toEqual([
'0123456789a',
'AbcDefXyz0123456789',
'λαπ',
])
}) })
}) })

View File

@ -25,10 +25,6 @@ export default {
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info) 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) => { CreateCategory: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Category'))) args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Category')))
return resolve(root, args, context, info) return resolve(root, args, context, info)

View File

@ -89,8 +89,10 @@ describe('slugify', () => {
}) })
describe('SignupVerification', () => { describe('SignupVerification', () => {
const mutation = `mutation($password: String!, $email: String!, $name: String!, $slug: String, $nonce: String!) { 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) { slug } SignupVerification(email: $email, password: $password, name: $name, slug: $slug, nonce: $nonce, termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion) {
slug
}
} }
` `
@ -98,7 +100,12 @@ describe('slugify', () => {
// required for SignupVerification // required for SignupVerification
await instance.create('EmailAddress', { email: '123@example.org', nonce: '123456' }) 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 }) return authenticatedClient.request(mutation, { ...defaultVariables, ...variables })
} }

View File

@ -80,9 +80,19 @@ module.exports = {
notifications: { notifications: {
type: 'relationship', type: 'relationship',
relationship: 'NOTIFIED', relationship: 'NOTIFIED',
target: 'Notification', target: 'User',
direction: 'in', direction: 'in',
}, },
termsAndConditionsAgreedVersion: {
type: 'string',
allow: [null],
},
/* termsAndConditionsAgreedAt: {
type: 'string',
isoDate: true,
allow: [null],
// required: true, TODO
}, */
shouted: { shouted: {
type: 'relationship', type: 'relationship',
relationship: 'SHOUTED', relationship: 'SHOUTED',

View File

@ -14,11 +14,13 @@ export default applyScalars(
query: { query: {
exclude: [ exclude: [
'Badge', 'Badge',
'Embed',
'InvitationCode', 'InvitationCode',
'EmailAddress', 'EmailAddress',
'Notfication', 'Notfication',
'Statistics', 'Statistics',
'LoggedInUser', 'LoggedInUser',
'Location',
'SocialMedia', 'SocialMedia',
'NOTIFIED', 'NOTIFIED',
], ],
@ -27,12 +29,19 @@ export default applyScalars(
mutation: { mutation: {
exclude: [ exclude: [
'Badge', 'Badge',
'Embed',
'InvitationCode', 'InvitationCode',
'EmailAddress', 'EmailAddress',
'Notfication', 'Notfication',
'Post',
'Comment',
'Report',
'Statistics', 'Statistics',
'LoggedInUser', 'LoggedInUser',
'Location',
'SocialMedia', 'SocialMedia',
'User',
'EMOTED',
'NOTIFIED', 'NOTIFIED',
], ],
// add 'User' here as soon as possible // add 'User' here as soon as possible

View File

@ -1,4 +1,4 @@
import { UserInputError } from 'apollo-server' import { ForbiddenError, UserInputError } from 'apollo-server'
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
import { neode } from '../../bootstrap/neo4j' import { neode } from '../../bootstrap/neo4j'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
@ -77,6 +77,12 @@ export default {
} }
}, },
SignupVerification: async (object, args, context, resolveInfo) => { 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 let { nonce, email } = args
email = email.toLowerCase() email = email.toLowerCase()
const result = await instance.cypher( const result = await instance.cypher(

View File

@ -34,6 +34,7 @@ describe('CreateInvitationCode', () => {
name: 'Inviter', name: 'Inviter',
email: 'inviter@example.org', email: 'inviter@example.org',
password: '1234', password: '1234',
termsAndConditionsAgreedVersion: '0.0.1',
} }
action = async () => { action = async () => {
const factory = Factory() const factory = Factory()
@ -293,19 +294,25 @@ describe('Signup', () => {
describe('SignupVerification', () => { describe('SignupVerification', () => {
const mutation = ` const mutation = `
mutation($name: String!, $password: String!, $email: String!, $nonce: String!) { mutation($name: String!, $password: String!, $email: String!, $nonce: String!, $termsAndConditionsAgreedVersion: String!) {
SignupVerification(name: $name, password: $password, email: $email, nonce: $nonce) { SignupVerification(name: $name, password: $password, email: $email, nonce: $nonce, termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion) {
id id
termsAndConditionsAgreedVersion
} }
} }
` `
describe('given valid password and email', () => { describe('given valid password and email', () => {
const variables = { let variables
nonce: '123456',
name: 'John Doe', beforeEach(async () => {
password: '123', variables = {
email: 'john@example.org', nonce: '123456',
} name: 'John Doe',
password: '123',
email: 'john@example.org',
termsAndConditionsAgreedVersion: '0.0.1',
}
})
describe('unauthenticated', () => { describe('unauthenticated', () => {
beforeEach(async () => { beforeEach(async () => {
@ -349,9 +356,9 @@ describe('SignupVerification', () => {
describe('sending a valid nonce', () => { describe('sending a valid nonce', () => {
it('creates a user account', async () => { it('creates a user account', async () => {
const expected = { const expected = {
SignupVerification: { SignupVerification: expect.objectContaining({
id: expect.any(String), id: expect.any(String),
}, }),
} }
await expect(client.request(mutation, variables)).resolves.toEqual(expected) 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' }) const { records: emails } = await instance.cypher(cypher, { name: 'John Doe' })
expect(emails).toHaveLength(1) 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', () => { describe('sending invalid nonce', () => {

View File

@ -43,14 +43,8 @@ export default {
'MATCH (r:Post) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countPosts', 'MATCH (r:Post) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countPosts',
countComments: countComments:
'MATCH (r:Comment) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countComments', 'MATCH (r:Comment) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countComments',
countNotifications: countNotifications: 'MATCH ()-[r:NOTIFIED]->() RETURN COUNT(r) AS countNotifications',
'MATCH (r:Notification) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countNotifications', countInvites: 'MATCH (r:InvitationCode) RETURN COUNT(r) AS countInvites',
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',
countFollows: 'MATCH (:User)-[r:FOLLOWS]->(:User) RETURN COUNT(r) AS countFollows', countFollows: 'MATCH (:User)-[r:FOLLOWS]->(:User) RETURN COUNT(r) AS countFollows',
countShouts: 'MATCH (:User)-[r:SHOUTED]->(:Post) RETURN COUNT(r) AS countShouts', countShouts: 'MATCH (:User)-[r:SHOUTED]->(:Post) RETURN COUNT(r) AS countShouts',
} }
@ -63,12 +57,6 @@ export default {
countNotifications: queryOne(queries.countNotifications, session).then( countNotifications: queryOne(queries.countNotifications, session).then(
res => res.countNotifications.low, 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), countInvites: queryOne(queries.countInvites, session).then(res => res.countInvites.low),
countFollows: queryOne(queries.countFollows, session).then(res => res.countFollows.low), countFollows: queryOne(queries.countFollows, session).then(res => res.countFollows.low),
countShouts: queryOne(queries.countShouts, session).then(res => res.countShouts.low), countShouts: queryOne(queries.countShouts, session).then(res => res.countShouts.low),

View File

@ -1,7 +1,6 @@
import encode from '../../jwt/encode' import encode from '../../jwt/encode'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
import { AuthenticationError } from 'apollo-server' import { AuthenticationError } from 'apollo-server'
import { neo4jgraphql } from 'neo4j-graphql-js'
import { neode } from '../../bootstrap/neo4j' import { neode } from '../../bootstrap/neo4j'
const instance = neode() const instance = neode()
@ -12,9 +11,9 @@ export default {
return Boolean(user && user.id) return Boolean(user && user.id)
}, },
currentUser: async (object, params, ctx, resolveInfo) => { currentUser: async (object, params, ctx, resolveInfo) => {
const { user } = ctx if (!ctx.user) return null
if (!user) return null const user = await instance.find('User', ctx.user.id)
return neo4jgraphql(object, { id: user.id }, ctx, resolveInfo, false) return user.toJson()
}, },
}, },
Mutation: { Mutation: {

View File

@ -1,7 +1,7 @@
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import { neode } from '../../bootstrap/neo4j' import { neode } from '../../bootstrap/neo4j'
import { UserInputError } from 'apollo-server' import { UserInputError, ForbiddenError } from 'apollo-server'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
const instance = neode() const instance = neode()
@ -88,6 +88,13 @@ export default {
return blockedUser.toJson() return blockedUser.toJson()
}, },
UpdateUser: async (object, args, context, resolveInfo) => { 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' }) args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
try { try {
const user = await instance.find('User', args.id) const user = await instance.find('User', args.id)
@ -158,6 +165,7 @@ export default {
}, },
...Resolver('User', { ...Resolver('User', {
undefinedToNull: [ undefinedToNull: [
'termsAndConditionsAgreedVersion',
'actorId', 'actorId',
'avatar', 'avatar',
'coverImg', 'coverImg',
@ -165,6 +173,8 @@ export default {
'disabled', 'disabled',
'locationName', 'locationName',
'about', 'about',
'termsAndConditionsAgreedVersion',
// TODO: 'termsAndConditionsAgreedAt',
], ],
boolean: { boolean: {
followedByCurrentUser: followedByCurrentUser:
@ -197,8 +207,6 @@ export default {
contributions: '-[:WROTE]->(related:Post)', contributions: '-[:WROTE]->(related:Post)',
comments: '-[:WROTE]->(related:Comment)', comments: '-[:WROTE]->(related:Comment)',
shouted: '-[:SHOUTED]->(related:Post)', shouted: '-[:SHOUTED]->(related:Post)',
organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)',
organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)',
categories: '-[:CATEGORIZED]->(related:Category)', categories: '-[:CATEGORIZED]->(related:Category)',
badges: '<-[:REWARDED]-(related:Badge)', badges: '<-[:REWARDED]-(related:Badge)',
}, },

View File

@ -75,22 +75,33 @@ describe('User', () => {
}) })
describe('UpdateUser', () => { describe('UpdateUser', () => {
const userParams = { let userParams
email: 'user@example.org', let variables
password: '1234',
id: 'u47', beforeEach(async () => {
name: 'John Doe', userParams = {
} email: 'user@example.org',
const variables = { password: '1234',
id: 'u47', id: 'u47',
name: 'John Doughnut', name: 'John Doe',
} }
variables = {
id: 'u47',
name: 'John Doughnut',
}
})
const updateUserMutation = gql` const updateUserMutation = gql`
mutation($id: ID!, $name: String) { mutation($id: ID!, $name: String, $termsAndConditionsAgreedVersion: String) {
UpdateUser(id: $id, name: $name) { UpdateUser(
id: $id
name: $name
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
) {
id id
name name
termsAndConditionsAgreedVersion
} }
} }
` `
@ -159,6 +170,30 @@ describe('UpdateUser', () => {
'child "name" fails because ["name" length must be at least 3 characters long]', '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

@ -44,8 +44,6 @@ type Statistics {
countPosts: Int! countPosts: Int!
countComments: Int! countComments: Int!
countNotifications: Int! countNotifications: Int!
countOrganizations: Int!
countProjects: Int!
countInvites: Int! countInvites: Int!
countFollows: Int! countFollows: Int!
countShouts: Int! countShouts: Int!
@ -70,13 +68,9 @@ enum Deletable {
enum ShoutTypeEnum { enum ShoutTypeEnum {
Post Post
Organization
Project
} }
enum FollowTypeEnum { enum FollowTypeEnum {
User User
Organization
Project
} }
type Reward { type Reward {
@ -87,21 +81,6 @@ type Reward {
badge: Badge @relation(name: "REWARDED", direction: "OUT") 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 { type SharedInboxEndpoint {
id: ID! id: ID!
uri: String uri: String

View File

@ -8,3 +8,14 @@ type EMOTED @relation(name: "EMOTED") {
createdAt: String createdAt: String
updatedAt: 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
}

View File

@ -1,6 +1,5 @@
type EmailAddress { type EmailAddress {
id: ID! email: ID!
email: String!
verifiedAt: String verifiedAt: String
createdAt: String createdAt: String
} }
@ -19,5 +18,6 @@ type Mutation {
avatarUpload: Upload avatarUpload: Upload
locationName: String locationName: String
about: String about: String
termsAndConditionsAgreedVersion: String
): User ): User
} }

View File

@ -52,6 +52,10 @@ type Post {
@cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)")
} }
input _PostInput {
id: ID!
}
type Mutation { type Mutation {
CreatePost( CreatePost(
id: ID id: ID
@ -77,6 +81,7 @@ type Mutation {
language: String language: String
categoryIds: [ID] categoryIds: [ID]
): Post ): Post
DeletePost(id: ID!): Post
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
} }

View File

@ -1,7 +1,6 @@
type Tag { type Tag {
id: ID! id: ID!
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN") 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)") 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)") taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)")
deleted: Boolean deleted: Boolean

View File

@ -1,173 +1,174 @@
type User { type User {
id: ID! id: ID!
actorId: String actorId: String
name: String name: String
email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email") email: String! @cypher(statement: "MATCH (this)-[: PRIMARY_EMAIL]->(e: EmailAddress) RETURN e.email")
slug: String! slug: String!
avatar: String avatar: String
coverImg: String coverImg: String
deleted: Boolean deleted: Boolean
disabled: Boolean disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN") disabledBy: User @relation(name: "DISABLED", direction: "IN")
role: UserGroup! role: UserGroup!
publicKey: String publicKey: String
invitedBy: User @relation(name: "INVITED", direction: "IN") invitedBy: User @relation(name: "INVITED", direction: "IN")
invited: [User] @relation(name: "INVITED", direction: "OUT") invited: [User] @relation(name: "INVITED", direction: "OUT")
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") location: Location @cypher(statement: "MATCH (this)-[: IS_IN]->(l: Location) RETURN l")
locationName: String locationName: String
about: String about: String
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN") socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
#createdAt: DateTime # createdAt: DateTime
#updatedAt: DateTime # updatedAt: DateTime
createdAt: String createdAt: String
updatedAt: String updatedAt: String
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") termsAndConditionsAgreedVersion: String
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
following: [User]! @relation(name: "FOLLOWS", direction: "OUT") friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)") friendsCount: Int! @cypher(statement: "MATCH (this)<-[: FRIENDS]->(r: User) RETURN COUNT(DISTINCT r)")
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") followingCount: Int! @cypher(statement: "MATCH (this)-[: FOLLOWS]->(r: User) RETURN COUNT(DISTINCT r)")
# Is the currently logged in user following that user? followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
followedByCurrentUser: Boolean! @cypher( followedByCount: Int! @cypher(statement: "MATCH (this)<-[: FOLLOWS]-(r: User) RETURN COUNT(DISTINCT r)")
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
"""
)
#contributions: [WrittenPost]! # Is the currently logged in user following that user?
#contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! followedByCurrentUser: Boolean! @cypher(
# @cypher( statement: """
# statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp" MATCH (this)<-[: FOLLOWS]-(u: User { id: $cypherParams.currentUserId})
# ) RETURN COUNT(u) >= 1
contributions: [Post]! @relation(name: "WROTE", direction: "OUT") """
contributionsCount: Int! @cypher( )
statement: """ isBlocked: Boolean! @cypher(
MATCH (this)-[:WROTE]->(r:Post) statement: """
WHERE NOT r.deleted = true AND NOT r.disabled = true MATCH (this)<-[: BLOCKED]-(u: User { id: $cypherParams.currentUserId})
RETURN COUNT(r) RETURN COUNT(u) >= 1
""" """
) )
comments: [Comment]! @relation(name: "WROTE", direction: "OUT") # contributions: [WrittenPost]!
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))") # 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") comments: [Comment]! @relation(name: "WROTE", 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)") 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") shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", 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") badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") badgesCount: Int! @cypher(statement: "MATCH (this)<-[: REWARDED]-(r: Badge) RETURN COUNT(r)")
emotions: [EMOTED] emotions: [EMOTED]
} }
input _UserFilter { input _UserFilter {
AND: [_UserFilter!] AND: [_UserFilter!]
OR: [_UserFilter!] OR: [_UserFilter!]
name_contains: String name_contains: String
about_contains: String about_contains: String
slug_contains: String slug_contains: String
id: ID id: ID
id_not: ID id_not: ID
id_in: [ID!] id_in: [ID!]
id_not_in: [ID!] id_not_in: [ID!]
id_contains: ID id_contains: ID
id_not_contains: ID id_not_contains: ID
id_starts_with: ID id_starts_with: ID
id_not_starts_with: ID id_not_starts_with: ID
id_ends_with: ID id_ends_with: ID
id_not_ends_with: ID id_not_ends_with: ID
friends: _UserFilter friends: _UserFilter
friends_not: _UserFilter friends_not: _UserFilter
friends_in: [_UserFilter!] friends_in: [_UserFilter!]
friends_not_in: [_UserFilter!] friends_not_in: [_UserFilter!]
friends_some: _UserFilter friends_some: _UserFilter
friends_none: _UserFilter friends_none: _UserFilter
friends_single: _UserFilter friends_single: _UserFilter
friends_every: _UserFilter friends_every: _UserFilter
following: _UserFilter following: _UserFilter
following_not: _UserFilter following_not: _UserFilter
following_in: [_UserFilter!] following_in: [_UserFilter!]
following_not_in: [_UserFilter!] following_not_in: [_UserFilter!]
following_some: _UserFilter following_some: _UserFilter
following_none: _UserFilter following_none: _UserFilter
following_single: _UserFilter following_single: _UserFilter
following_every: _UserFilter following_every: _UserFilter
followedBy: _UserFilter followedBy: _UserFilter
followedBy_not: _UserFilter followedBy_not: _UserFilter
followedBy_in: [_UserFilter!] followedBy_in: [_UserFilter!]
followedBy_not_in: [_UserFilter!] followedBy_not_in: [_UserFilter!]
followedBy_some: _UserFilter followedBy_some: _UserFilter
followedBy_none: _UserFilter followedBy_none: _UserFilter
followedBy_single: _UserFilter followedBy_single: _UserFilter
followedBy_every: _UserFilter followedBy_every: _UserFilter
} }
type Query { type Query {
User( User(
id: ID id: ID
email: String email: String
actorId: String actorId: String
name: String name: String
slug: String slug: String
avatar: String avatar: String
coverImg: String coverImg: String
role: UserGroup role: UserGroup
locationName: String locationName: String
about: String about: String
createdAt: String createdAt: String
updatedAt: String updatedAt: String
friendsCount: Int friendsCount: Int
followingCount: Int followingCount: Int
followedByCount: Int followedByCount: Int
followedByCurrentUser: Boolean followedByCurrentUser: Boolean
contributionsCount: Int contributionsCount: Int
commentedCount: Int commentedCount: Int
shoutedCount: Int shoutedCount: Int
badgesCount: Int badgesCount: Int
first: Int first: Int
offset: Int offset: Int
orderBy: [_UserOrdering] orderBy: [_UserOrdering]
filter: _UserFilter filter: _UserFilter
): [User] ): [User]
blockedUsers: [User] blockedUsers: [User]
currentUser: User
} }
type Mutation { type Mutation {
UpdateUser ( UpdateUser (
id: ID! id: ID!
name: String name: String
email: String email: String
slug: String slug: String
avatar: String avatar: String
coverImg: String coverImg: String
avatarUpload: Upload avatarUpload: Upload
locationName: String locationName: String
about: String about: String
): User termsAndConditionsAgreedVersion: String
): User
DeleteUser(id: ID!, resource: [Deletable]): User DeleteUser(id: ID!, resource: [Deletable]): User
block(id: ID!): User block(id: ID!): User
unblock(id: ID!): User unblock(id: ID!): User
} }

View File

@ -14,6 +14,8 @@ export default function create() {
role: 'user', role: 'user',
avatar: faker.internet.avatar(), avatar: faker.internet.avatar(),
about: faker.lorem.paragraph(), about: faker.lorem.paragraph(),
// termsAndConditionsAgreedAt: new Date().toISOString(),
termsAndConditionsAgreedVersion: '0.0.1',
} }
defaults.slug = slugify(defaults.name, { lower: true }) defaults.slug = slugify(defaults.name, { lower: true })
args = { args = {

View File

@ -681,7 +681,7 @@
pirates "^4.0.0" pirates "^4.0.0"
source-map-support "^0.5.9" 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" version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.5.5.tgz#c3214c08ef20341af4187f1c9fbdc357fbec96b2" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.5.5.tgz#c3214c08ef20341af4187f1c9fbdc357fbec96b2"
integrity sha512-FYATQVR00NSNi7mUfpPDp7E8RYMXDuO8gaix7u/w3GekfUinKgX1AcTxs7SoiEmoEW9mbpjrwqWSW6zCmw5h8A== integrity sha512-FYATQVR00NSNi7mUfpPDp7E8RYMXDuO8gaix7u/w3GekfUinKgX1AcTxs7SoiEmoEW9mbpjrwqWSW6zCmw5h8A==
@ -5811,10 +5811,10 @@ mem@~5.1.1:
mimic-fn "^2.1.0" mimic-fn "^2.1.0"
p-is-promise "^2.1.0" p-is-promise "^2.1.0"
memoize-one@~5.1.0: memoize-one@~5.1.1:
version "5.1.0" version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.0.tgz#ce7af291c0e2fe041b709cac5e8c7b198c994286" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-p3tPVJNrjOkJ0vk0FRn6yv898qlQZct1rsQAXuwK9X5brNVajPv/y13ytrUByzSS8olyzeqCLX8BKEWmTmXa1A== integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
memorystream@^0.3.1: memorystream@^0.3.1:
version "0.3.1" version "0.3.1"
@ -5912,14 +5912,14 @@ metascraper-publisher@^5.6.5:
dependencies: dependencies:
"@metascraper/helpers" "^5.6.6" "@metascraper/helpers" "^5.6.6"
metascraper-soundcloud@^5.6.5: metascraper-soundcloud@^5.6.7:
version "5.6.6" version "5.6.7"
resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.6.6.tgz#7f609cd1f5ad4d466d8ac5e232bb3e2c9608ec24" resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.6.7.tgz#c06a5398c85ec2d8e139c7f2016451ebf54de039"
integrity sha512-8p3tI9BbD3MKv37t5gsh8E5ZDMZvZLKbLqK6hKEG2v0I0fzmpDQdUwhgo6NI4eAmOrPsyiPeQmu6sZLTWeK3OA== integrity sha512-pHfhrAej8adCwK5Vo0IIOvoCTSWM1gSXeUQAwSyPGySME7NRMfgMq/TJSFF24Qih0UrE7u/4ogtGR3k3/Sno6g==
dependencies: dependencies:
"@metascraper/helpers" "^5.6.6" "@metascraper/helpers" "^5.6.6"
memoize-one "~5.1.0" memoize-one "~5.1.1"
tldts "~5.3.2" tldts "~5.4.0"
metascraper-title@^5.6.5: metascraper-title@^5.6.5:
version "5.6.6" version "5.6.6"
@ -5944,15 +5944,15 @@ metascraper-video@^5.6.5:
"@metascraper/helpers" "^5.6.6" "@metascraper/helpers" "^5.6.6"
lodash "~4.17.15" lodash "~4.17.15"
metascraper-youtube@^5.6.3: metascraper-youtube@^5.6.7:
version "5.6.6" version "5.6.7"
resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.6.6.tgz#ba098f1c92306e39645a11ace263c24d77223a18" resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.6.7.tgz#48c68aefb8b67aaae1df532dad8b4c0d6e8b7240"
integrity sha512-xV9uyMNUdXhsNMqnFk7bde2ygt2rKHJ8r+3Ft+GbgP+Zdw6PXOCy6/+yA/kF5nZouF7DOBBTw2bWEXqu08eCmQ== integrity sha512-8/v+PwkNyVgRrD5SVVXH7aY1ld0NVlHlz/RhX6sre9OoYY70axa+zNttEp15zGcbOpS+g3RtEb7So/fbyb/Xcg==
dependencies: dependencies:
"@metascraper/helpers" "^5.6.6" "@metascraper/helpers" "^5.6.6"
get-video-id "~3.1.4" get-video-id "~3.1.4"
is-reachable "~3.1.0" is-reachable "~3.1.0"
memoize-one "~5.1.0" memoize-one "~5.1.1"
p-locate "~4.1.0" p-locate "~4.1.0"
metascraper@^4.10.3: 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" resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.203.1.tgz#4dc9b02f53de3315bc98b80665e13de3edfc1dfc"
integrity sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw== integrity sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw==
tldts-core@^5.3.2: tldts-core@^5.4.0:
version "5.3.2" version "5.4.0"
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.3.2.tgz#3823653310c2dc6e51d00998098a72a3cc203ea9" resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.4.0.tgz#bd69ca3ad96a78ab675c74684c6d0717929777ec"
integrity sha512-bGI7MvrFXnbNhSkKEKAjiRo+eoIuIsuzF/hOtpI7HLMLWamIGtrLuNv2nlBOdN/h6iB6B32B4MxmwPWTZ70TaQ== integrity sha512-kfDnB7fcNgNUcn5k21TPM/MbXqJNt2bBGQRfGyE39H334Qk+qNcSqw9It3YPxvrA7msl7DQ8wvcIsa0y55auug==
tldts@~5.3.2: tldts@~5.4.0:
version "5.3.2" version "5.4.0"
resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.3.2.tgz#d7e9b31b70b7d7687a16fe31a18ec2ca8a33f852" resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.4.0.tgz#ded769383615341660243acf547f197eb48f6795"
integrity sha512-R96Q0sJoD1vMYZ5YI0om32AZW2SYDxWfcg0111gnJYIvfyVgHOwyYj2dLFhpkPCSH7AwwUe0MmewmUrQhJXRjQ== integrity sha512-S1CmstJxRb6KK+uLHhMUXXkI/zjA20RGM9QYkLgDEj42C9Zmr+OLjzEqf4Id/EWppuLi1z9FdNsz8/qi/pLCGA==
dependencies: dependencies:
tldts-core "^5.3.2" tldts-core "^5.4.0"
tmp@^0.0.33: tmp@^0.0.33:
version "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" resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc"
integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw= 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: xtend@^4.0.1:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"

View File

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

View File

@ -31,6 +31,7 @@ Given('I am logged in with a {string} role', role => {
cy.factory().create('User', { cy.factory().create('User', {
email: `${role}@example.org`, email: `${role}@example.org`,
password: '1234', password: '1234',
termsAndConditionsAgreedVersion: "0.0.2",
role role
}) })
cy.login({ 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"; import helpers from "../../support/helpers";
/* global cy */ /* global cy */
@ -9,12 +13,16 @@ let loginCredentials = {
email: "peterpan@example.org", email: "peterpan@example.org",
password: "1234" password: "1234"
}; };
const termsAndConditionsAgreedVersion = {
termsAndConditionsAgreedVersion: "0.0.2"
};
const narratorParams = { const narratorParams = {
id: 'id-of-peter-pan', id: 'id-of-peter-pan',
name: "Peter Pan", name: "Peter Pan",
slug: "peter-pan", slug: "peter-pan",
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg", avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
...loginCredentials ...loginCredentials,
...termsAndConditionsAgreedVersion,
}; };
Given("I am logged in", () => { 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", () => { Given("we have a selection of tags and categories as well as posts", () => {
cy.createCategories("cat12") cy.createCategories("cat12")
.factory() .factory()
.create("Tag", { id: "Ecology" }) .create("Tag", {
.create("Tag", { id: "Nature" }) id: "Ecology"
.create("Tag", { id: "Democracy" }); })
.create("Tag", {
id: "Nature"
})
.create("Tag", {
id: "Democracy"
});
cy.factory() cy.factory()
.create("User", { id: 'a1' }) .create("User", {
.create("Post", {authorId: 'a1', tagIds: [ "Ecology", "Nature", "Democracy" ], categoryIds: ["cat12"] }) id: 'a1'
.create("Post", {authorId: 'a1', tagIds: [ "Nature", "Democracy" ], categoryIds: ["cat121"] }); })
.create("Post", {
authorId: 'a1',
tagIds: ["Ecology", "Nature", "Democracy"],
categoryIds: ["cat12"]
})
.create("Post", {
authorId: 'a1',
tagIds: ["Nature", "Democracy"],
categoryIds: ["cat121"]
});
cy.factory() cy.factory()
.create("User", { id: 'a2'}) .create("User", {
.create("Post", { authorId: 'a2', tagIds: ['Nature', 'Democracy'], categoryIds: ["cat12"] }); id: 'a2'
})
.create("Post", {
authorId: 'a2',
tagIds: ['Nature', 'Democracy'],
categoryIds: ["cat12"]
});
cy.factory() 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 => { Given("we have the following user accounts:", table => {
table.hashes().forEach(params => { 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 => { Given("my user account has the role {string}", role => {
cy.factory().create("User", { cy.factory().create("User", {
role, 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 => { 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.get(`html[lang=${code}]`);
cy.getCookie("locale").should("have.property", "value", 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" icon: "smile"
}) })
table.hashes().forEach(({ ...postAttributes }, i) => { table.hashes().forEach(({
...postAttributes
}, i) => {
postAttributes = { postAttributes = {
...postAttributes, ...postAttributes,
deleted: Boolean(postAttributes.deleted), deleted: Boolean(postAttributes.deleted),
@ -229,10 +271,15 @@ Then("the first post on the landing page has the title:", title => {
Then( Then(
"the page {string} returns a 404 error with a message:", "the page {string} returns a 404 error with a message:",
(route, message) => { (route, message) => {
cy.request({ url: route, failOnStatusCode: false }) cy.request({
url: route,
failOnStatusCode: false
})
.its("status") .its("status")
.should("eq", 404); .should("eq", 404);
cy.visit(route, { failOnStatusCode: false }); cy.visit(route, {
failOnStatusCode: false
});
cy.get(".error").should("contain", message); cy.get(".error").should("contain", message);
} }
); );
@ -240,7 +287,10 @@ Then(
Given("my user account has the following login credentials:", table => { Given("my user account has the following login credentials:", table => {
loginCredentials = table.hashes()[0]; loginCredentials = table.hashes()[0];
cy.debug(); cy.debug();
cy.factory().create("User", loginCredentials); cy.factory().create("User", {
...termsAndConditionsAgreedVersion,
...loginCredentials
});
}); });
When("I fill the password form with:", table => { 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 => { Then("I cannot login anymore with password {string}", password => {
cy.reload(); cy.reload();
const { email } = loginCredentials; const {
email
} = loginCredentials;
cy.visit(`/login`); cy.visit(`/login`);
cy.get("input[name=email]") cy.get("input[name=email]")
.trigger("focus") .trigger("focus")
@ -280,14 +332,22 @@ Then("I can login successfully with password {string}", password => {
cy.reload(); cy.reload();
cy.login({ cy.login({
...loginCredentials, ...loginCredentials,
...{ password } ...{
password
}
}); });
cy.get(".iziToast-wrapper").should("contain", "You are logged in!"); cy.get(".iziToast-wrapper").should("contain", "You are logged in!");
}); });
When("I log in with the following credentials:", table => { When("I log in with the following credentials:", table => {
const { email, password } = table.hashes()[0]; const {
cy.login({ email, password }); email,
password
} = table.hashes()[0];
cy.login({
email,
password
});
}); });
When("open the notification menu and click on the first item", () => { 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 => { Given("there is an annoying user called {string}", name => {
const annoyingParams = { const annoyingParams = {
email: "spammy-spammer@example.org", email: "spammy-spammer@example.org",
password: "1234" password: "1234",
...termsAndConditionsAgreedVersion
}; };
cy.factory().create("User", { cy.factory().create("User", {
...annoyingParams, ...annoyingParams,
id: "annoying-user", id: "annoying-user",
name name,
...termsAndConditionsAgreedVersion,
}); });
}); });
@ -364,7 +426,9 @@ When(
cy.get(".user-content-menu .content-menu-trigger").click(); cy.get(".user-content-menu .content-menu-trigger").click();
cy.get(".popover .ds-menu-item-link") cy.get(".popover .ds-menu-item-link")
.contains(button) .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 => { Given("I follow the user {string}", name => {
cy.neode() cy.neode()
.first("User", { name }) .first("User", {
name
})
.then(followed => { .then(followed => {
cy.neode() cy.neode()
.first("User", { name: narratorParams.name }) .first("User", {
name: narratorParams.name
})
.relateTo(followed, "following"); .relateTo(followed, "following");
}); });
}); });
@ -390,7 +458,11 @@ Given("I follow the user {string}", name => {
Given('"Spammy Spammer" wrote a post {string}', title => { Given('"Spammy Spammer" wrote a post {string}', title => {
cy.createCategories("cat21") cy.createCategories("cat21")
.factory() .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", () => { 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 => { Given("I wrote a post {string}", title => {
cy.createCategories(`cat213`, title) cy.createCategories(`cat213`, title)
.factory() .factory()
.create("Post", { authorId: narratorParams.id, title, categoryIds: ["cat213"] }); .create("Post", {
authorId: narratorParams.id,
title,
categoryIds: ["cat213"]
});
}); });
When("I block the user {string}", name => { When("I block the user {string}", name => {
cy.neode() cy.neode()
.first("User", { name }) .first("User", {
name
})
.then(blocked => { .then(blocked => {
cy.neode() cy.neode()
.first("User", { name: narratorParams.name }) .first("User", {
name: narratorParams.name
})
.relateTo(blocked, "blocked"); .relateTo(blocked, "blocked");
}); });
}); });
When("I log in with:", table => { When("I log in with:", table => {
const [firstRow] = table.hashes(); const [firstRow] = table.hashes();
const { Email, Password } = firstRow; const {
cy.login({ email: Email, password: Password }); Email,
Password
} = firstRow;
cy.login({
email: Email,
password: Password
});
}); });
Then("I see only one post with the title {string}", title => { 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") .find(".post-link")
.should("have.length", 1); .should("have.length", 1);
cy.get(".main-container").contains(".post-link", title); cy.get(".main-container").contains(".post-link", title);
}); });

View File

@ -33,20 +33,20 @@ export_collection "categories"
export_collection "comments" export_collection "comments"
export_collection_query "contributions" '{"type": "DELETED"}' "DELETED" export_collection_query "contributions" '{"type": "DELETED"}' "DELETED"
export_collection_query "contributions" '{"type": "post"}' "post" 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 "emotions"
export_collection_query "follows" '{"foreignService": "organizations"}' "organizations" # export_collection_query "follows" '{"foreignService": "organizations"}' "organizations"
export_collection_query "follows" '{"foreignService": "users"}' "users" export_collection_query "follows" '{"foreignService": "users"}' "users"
export_collection "invites" # export_collection "invites"
export_collection "organizations" # export_collection "organizations"
export_collection "pages" # export_collection "pages"
export_collection "projects" # export_collection "projects"
export_collection "settings" # export_collection "settings"
export_collection "shouts" export_collection "shouts"
export_collection "status" # export_collection "status"
export_collection "users" export_collection "users"
export_collection "userscandos" # export_collection "userscandos"
export_collection "usersettings" # export_collection "usersettings"
# Close SSH Tunnel # Close SSH Tunnel
ssh -S my-ctrl-socket -O check -l ${SSH_USERNAME} ${SSH_HOST} ssh -S my-ctrl-socket -O check -l ${SSH_USERNAME} ${SSH_HOST}

View File

@ -125,7 +125,6 @@
[ ] wasSeeded: { type: Boolean } [ ] wasSeeded: { type: Boolean }
} }
*/ */
CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as post CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as post
MERGE (p:Post {id: post._id["$oid"]}) MERGE (p:Post {id: post._id["$oid"]})
ON CREATE SET ON CREATE SET
@ -148,6 +147,10 @@ MATCH (c:Category {id: categoryId})
MERGE (p)-[:CATEGORIZED]->(c) MERGE (p)-[:CATEGORIZED]->(c)
WITH p, post.tags AS tags WITH p, post.tags AS tags
UNWIND tags AS tag 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) MERGE (p)-[:TAGGED]->(t)
; ;

View File

@ -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:Comment) ASSERT c.id IS UNIQUE;
CREATE CONSTRAINT ON (c:Category) 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 (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 (t:Tag) ASSERT t.id IS UNIQUE;
CREATE CONSTRAINT ON (p:Post) ASSERT p.slug 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 (c:Category) ASSERT c.slug IS UNIQUE;
CREATE CONSTRAINT ON (u:User) ASSERT u.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; CREATE CONSTRAINT ON (e:EmailAddress) ASSERT e.email IS UNIQUE;
' | cypher-shell ' | cypher-shell

View File

@ -26,6 +26,7 @@ import { Editor, EditorContent } from 'tiptap'
import { History } from 'tiptap-extensions' import { History } from 'tiptap-extensions'
import linkify from 'linkify-it' import linkify from 'linkify-it'
import stringHash from 'string-hash' import stringHash from 'string-hash'
import { replace, build } from 'xregexp/xregexp-all.js'
import * as key from '../../constants/keycodes' import * as key from '../../constants/keycodes'
import { HASHTAG, MENTION } from '../../constants/editor' import { HASHTAG, MENTION } from '../../constants/editor'
@ -214,8 +215,9 @@ export default {
}, },
sanitizeQuery(query) { sanitizeQuery(query) {
if (this.suggestionType === HASHTAG) { if (this.suggestionType === HASHTAG) {
// remove all not allowed chars // remove all non unicode letters and non digits
query = query.replace(/[^a-zA-Z0-9]/gm, '') const regexMatchAllNonUnicodeLettersOrDigits = build('[^\\pL0-9]')
query = replace(query, regexMatchAllNonUnicodeLettersOrDigits, '', 'all')
// if the query is only made of digits, make it empty // if the query is only made of digits, make it empty
return query.replace(/[0-9]/gm, '') === '' ? '' : query return query.replace(/[0-9]/gm, '') === '' ? '' : query
} }

View File

@ -7,15 +7,15 @@
:class="{ 'is-selected': navigatedItemIndex === index }" :class="{ 'is-selected': navigatedItemIndex === index }"
@click="selectItem(item)" @click="selectItem(item)"
> >
{{ createItemLabel(item) }} {{ createItemLabel(item) | truncate(50) }}
</li> </li>
<template v-if="isHashtag"> <template v-if="isHashtag">
<li v-if="!query" class="suggestion-list__item hint"> <li v-if="!query" class="suggestion-list__item hint">{{ $t('editor.hashtag.addLetter') }}</li>
{{ $t('editor.hashtag.addLetter') }}
</li>
<template v-else-if="!filteredItems.find(el => el.id === query)"> <template v-else-if="!filteredItems.find(el => el.id === query)">
<li class="suggestion-list__item hint">{{ $t('editor.hashtag.addHashtag') }}</li> <li class="suggestion-list__item hint">{{ $t('editor.hashtag.addHashtag') }}</li>
<li class="suggestion-list__item" @click="selectItem({ id: query })">#{{ query }}</li> <li class="suggestion-list__item" @click="selectItem({ id: query })">
#{{ query | truncate(50) }}
</li>
</template> </template>
</template> </template>
<template v-else-if="isMention"> <template v-else-if="isMention">

View File

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

View File

@ -55,7 +55,10 @@
v-model="termsAndConditionsConfirmed" v-model="termsAndConditionsConfirmed"
:checked="termsAndConditionsConfirmed" :checked="termsAndConditionsConfirmed"
/> />
<label for="checkbox" v-html="$t('site.termsAndConditionsConfirmed')"></label> <label
for="checkbox"
v-html="$t('termsAndConditions.termsAndConditionsConfirmed')"
></label>
</ds-text> </ds-text>
<template slot="footer"> <template slot="footer">
@ -84,9 +87,24 @@ import gql from 'graphql-tag'
import PasswordStrength from '../Password/Strength' import PasswordStrength from '../Password/Strength'
import { SweetalertIcon } from 'vue-sweetalert-icons' import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper' 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` export const SignupVerificationMutation = gql`
mutation($nonce: String!, $name: String!, $email: String!, $password: String!) { mutation(
SignupVerification(nonce: $nonce, email: $email, name: $name, password: $password) { $nonce: String!
$name: String!
$email: String!
$password: String!
$termsAndConditionsAgreedVersion: String!
) {
SignupVerification(
nonce: $nonce
email: $email
name: $name
password: $password
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
) {
id id
name name
slug slug
@ -135,10 +153,11 @@ export default {
async submit() { async submit() {
const { name, password, about } = this.formData const { name, password, about } = this.formData
const { email, nonce } = this const { email, nonce } = this
const termsAndConditionsAgreedVersion = VERSION
try { try {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: SignupVerificationMutation, mutation: SignupVerificationMutation,
variables: { name, password, about, email, nonce }, variables: { name, password, about, email, nonce, termsAndConditionsAgreedVersion },
}) })
this.success = true this.success = true
setTimeout(() => { setTimeout(() => {

View File

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

View File

@ -6,9 +6,9 @@
<ds-flex class="main-navigation-flex" centered> <ds-flex class="main-navigation-flex" centered>
<ds-flex-item :width="{ lg: '3.5%' }" /> <ds-flex-item :width="{ lg: '3.5%' }" />
<ds-flex-item :width="{ base: '80%', sm: '80%', md: '80%', lg: '15%' }"> <ds-flex-item :width="{ base: '80%', sm: '80%', md: '80%', lg: '15%' }">
<a @click="redirectToRoot"> <nuxt-link :to="{ name: 'index' }">
<ds-logo /> <ds-logo />
</a> </nuxt-link>
</ds-flex-item> </ds-flex-item>
<ds-flex-item <ds-flex-item
:width="{ base: '20%', sm: '20%', md: '20%', lg: '0%' }" :width="{ base: '20%', sm: '20%', md: '20%', lg: '0%' }"
@ -147,7 +147,7 @@
</template> </template>
<script> <script>
import { mapGetters, mapActions, mapMutations } from 'vuex' import { mapGetters, mapActions } from 'vuex'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch' import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import SearchInput from '~/components/SearchInput.vue' import SearchInput from '~/components/SearchInput.vue'
import Modal from '~/components/Modal' import Modal from '~/components/Modal'
@ -183,8 +183,6 @@ export default {
isAdmin: 'auth/isAdmin', isAdmin: 'auth/isAdmin',
quickSearchResults: 'search/quickResults', quickSearchResults: 'search/quickResults',
quickSearchPending: 'search/quickPending', quickSearchPending: 'search/quickPending',
usersFollowedFilter: 'posts/usersFollowedFilter',
categoriesFilter: 'posts/categoriesFilter',
}), }),
userName() { userName() {
const { name } = this.user || {} const { name } = this.user || {}
@ -224,7 +222,7 @@ export default {
}, },
showFilterPostsDropdown() { showFilterPostsDropdown() {
const [firstRoute] = this.$route.matched const [firstRoute] = this.$route.matched
return firstRoute.name === 'index' return firstRoute && firstRoute.name === 'index'
}, },
}, },
watch: { watch: {
@ -236,10 +234,6 @@ export default {
...mapActions({ ...mapActions({
quickSearchClear: 'search/quickClear', quickSearchClear: 'search/quickClear',
quickSearch: 'search/quickSearch', quickSearch: 'search/quickSearch',
fetchPosts: 'posts/fetchPosts',
}),
...mapMutations({
setFilteredByFollowers: 'posts/SET_FILTERED_BY_FOLLOWERS',
}), }),
goToPost(item) { goToPost(item) {
this.$nextTick(() => { this.$nextTick(() => {
@ -259,17 +253,6 @@ export default {
toggleMobileMenuView() { toggleMobileMenuView() {
this.toggleMobileMenu = !this.toggleMobileMenu this.toggleMobileMenu = !this.toggleMobileMenu
}, },
redirectToRoot() {
this.$router.replace('/')
this.fetchPosts({
i18n: this.$i18n,
filter: {
...this.usersFollowedFilter,
...this.categoriesFilter,
...this.filter,
},
})
},
}, },
apollo: { apollo: {
Category: { Category: {

View File

@ -21,6 +21,7 @@
} }
}, },
"site": { "site": {
"thanks": "Danke!",
"made": "Mit &#10084; gemacht", "made": "Mit &#10084; gemacht",
"imprint": "Impressum", "imprint": "Impressum",
"data-privacy": "Datenschutz", "data-privacy": "Datenschutz",
@ -34,14 +35,11 @@
"responsible": "Verantwortlicher gemäß § 55 Abs. 2 RStV ", "responsible": "Verantwortlicher gemäß § 55 Abs. 2 RStV ",
"bank": "Bankverbindung", "bank": "Bankverbindung",
"germany": "Deutschland", "germany": "Deutschland",
"code-of-conduct": "Verhaltenscodex", "code-of-conduct": "Verhaltenscodex"
"termsAndConditionsConfirmed": "Ich habe die <a href=\"/terms-and-conditions\" target=\"_blank\">Nutzungsbedingungen</a> durchgelesen und stimme ihnen zu."
}, },
"sorting": { "sorting": {
"newest": "Neuste", "newest": "Neuste",
"oldest": "Älteste", "oldest": "Älteste"
"popular": "Beliebt",
"commented": "meist Kommentiert"
}, },
"login": { "login": {
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.", "copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
@ -575,6 +573,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:" "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": { "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": { "risk": {
"title": "Unfallgefahr", "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." "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."

View File

@ -21,6 +21,7 @@
} }
}, },
"site": { "site": {
"thanks": "Thanks!",
"made": "Made with &#10084;", "made": "Made with &#10084;",
"imprint": "Imprint", "imprint": "Imprint",
"termsAndConditions": "Terms and conditions", "termsAndConditions": "Terms and conditions",
@ -34,14 +35,11 @@
"responsible": "responsible for contents of this page (§ 55 Abs. 2 RStV)", "responsible": "responsible for contents of this page (§ 55 Abs. 2 RStV)",
"bank": "bank account", "bank": "bank account",
"germany": "Germany", "germany": "Germany",
"code-of-conduct": "Code of Conduct", "code-of-conduct": "Code of Conduct"
"termsAndConditionsConfirmed": "I have read and confirmed the <a href=\"/terms-and-conditions\" target=\"_blank\">terms and conditions</a>."
}, },
"sorting": { "sorting": {
"newest": "Newest", "newest": "Newest",
"oldest": "Oldest", "oldest": "Oldest"
"popular": "Popular",
"commented": "Most commented"
}, },
"login": { "login": {
"copy": "If you already have a human-connection account, login here.", "copy": "If you already have a human-connection account, login here.",
@ -575,6 +573,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:" "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": { "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": { "risk": {
"title": "Risk of accident", "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." "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."

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: { router: {
middleware: ['authenticated'], middleware: ['authenticated', 'termsAndConditions'],
linkActiveClass: 'router-link-active', linkActiveClass: 'router-link-active',
linkExactActiveClass: 'router-link-exact-active', linkExactActiveClass: 'router-link-exact-active',
scrollBehavior: (to, _from, savedPosition) => { scrollBehavior: (to, _from, savedPosition) => {

View File

@ -81,6 +81,7 @@
"vue-izitoast": "^1.2.1", "vue-izitoast": "^1.2.1",
"vue-sweetalert-icons": "~4.2.0", "vue-sweetalert-icons": "~4.2.0",
"vuex-i18n": "~1.13.1", "vuex-i18n": "~1.13.1",
"xregexp": "^4.2.4",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -49,34 +49,6 @@
</ds-number> </ds-number>
</ds-space> </ds-space>
</ds-flex-item> </ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.organizations')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="statistics.countOrganizations" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.projects')"
size="x-large"
uppercase
>
<client-only slot="count">
<hc-count-to :end-val="statistics.countProjects" />
</client-only>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }"> <ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small"> <ds-space margin="small">
<ds-number :count="0" :label="$t('admin.dashboard.invites')" size="x-large" uppercase> <ds-number :count="0" :label="$t('admin.dashboard.invites')" size="x-large" uppercase>
@ -137,8 +109,6 @@ export default {
countPosts countPosts
countComments countComments
countNotifications countNotifications
countOrganizations
countProjects
countInvites countInvites
countFollows countFollows
countShouts countShouts

View File

@ -105,18 +105,6 @@ export default {
icons: 'sort-amount-asc', icons: 'sort-amount-asc',
order: 'createdAt_asc', order: 'createdAt_asc',
}, },
{
label: this.$t('sorting.popular'),
value: 'Popular',
icons: 'fire',
order: 'shoutedCount_desc',
},
{
label: this.$t('sorting.commented'),
value: 'Commented',
icons: 'comment',
order: 'commentsCount_desc',
},
], ],
} }
}, },

View File

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

View File

@ -178,6 +178,7 @@ export default {
.post-card { .post-card {
margin: auto; margin: auto;
cursor: auto;
.comments { .comments {
margin-top: $space-small; margin-top: $space-small;

View File

@ -33,19 +33,18 @@
</div> </div>
</template> </template>
<h3>{{ $t('post.moreInfo.titleOfRelatedContributionsSection') }}</h3> <h3>{{ $t('post.moreInfo.titleOfRelatedContributionsSection') }}</h3>
<ds-section style="margin: 0 -1.5rem; padding: 1.5rem;"> <ds-section>
<ds-flex v-if="post.relatedContributions && post.relatedContributions.length" gutter="small"> <masonry-grid v-if="post.relatedContributions && post.relatedContributions.length">
<hc-post-card <masonry-grid-item v-for="relatedPost in post.relatedContributions" :key="relatedPost.id">
v-for="relatedPost in post.relatedContributions" <hc-post-card
:key="relatedPost.id" :post="relatedPost"
:post="relatedPost" :width="{ base: '100%', lg: 1 }"
:width="{ base: '100%', lg: 1 }" @removePostFromList="removePostFromList"
@removePostFromList="removePostFromList" />
/> </masonry-grid-item>
</ds-flex> </masonry-grid>
<hc-empty v-else margin="large" icon="file" message="No related Posts" /> <hc-empty v-else margin="large" icon="file" message="No related Posts" />
</ds-section> </ds-section>
<ds-space margin-bottom="large" />
</ds-card> </ds-card>
</template> </template>
@ -53,6 +52,8 @@
import HcPostCard from '~/components/PostCard' import HcPostCard from '~/components/PostCard'
import HcEmpty from '~/components/Empty.vue' import HcEmpty from '~/components/Empty.vue'
import { relatedContributions } from '~/graphql/PostQuery' import { relatedContributions } from '~/graphql/PostQuery'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
export default { export default {
transition: { transition: {
@ -62,6 +63,8 @@ export default {
components: { components: {
HcPostCard, HcPostCard,
HcEmpty, HcEmpty,
MasonryGrid,
MasonryGridItem,
}, },
computed: { computed: {
post() { post() {

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 gql from 'graphql-tag'
import { VERSION } from '~/constants/terms-and-conditions-version.js'
export const state = () => { export const state = () => {
return { return {
@ -42,6 +43,9 @@ export const getters = {
token(state) { token(state) {
return state.token return state.token
}, },
termsAndConditionsAgreed(state) {
return state.user && state.user.termsAndConditionsAgreedVersion === VERSION
},
} }
export const actions = { export const actions = {
@ -82,6 +86,7 @@ export const actions = {
locationName locationName
contributionsCount contributionsCount
commentedCount commentedCount
termsAndConditionsAgreedVersion
socialMedia { socialMedia {
id id
url url

View File

@ -1,119 +0,0 @@
import gql from 'graphql-tag'
export const state = () => {
return {
posts: [],
filteredByUsersFollowed: false,
filteredByCategories: false,
usersFollowedFilter: {},
categoriesFilter: {},
selectedCategoryIds: [],
}
}
export const mutations = {
SET_POSTS(state, posts) {
state.posts = posts || null
},
SET_FILTERED_BY_FOLLOWERS(state, boolean) {
state.filteredByUsersFollowed = boolean || null
},
SET_FILTERED_BY_CATEGORIES(state, boolean) {
state.filteredByCategories = boolean || null
},
SET_USERS_FOLLOWED_FILTER(state, filter) {
state.usersFollowedFilter = filter || null
},
SET_CATEGORIES_FILTER(state, filter) {
state.categoriesFilter = filter || null
},
SET_SELECTED_CATEGORY_IDS(state, categoryId) {
if (!categoryId) {
state.selectedCategoryIds = []
} else {
const index = state.selectedCategoryIds.indexOf(categoryId)
if (index > -1) {
state.selectedCategoryIds.splice(index, 1)
} else {
state.selectedCategoryIds.push(categoryId)
}
}
},
}
export const getters = {
posts(state) {
return state.posts || []
},
filteredByUsersFollowed(state) {
return state.filteredByUsersFollowed || false
},
filteredByCategories(state) {
return state.filteredByCategories || false
},
usersFollowedFilter(state) {
return state.usersFollowedFilter || {}
},
categoriesFilter(state) {
return state.categoriesFilter || {}
},
selectedCategoryIds(state) {
return state.selectedCategoryIds || []
},
}
export const actions = {
async fetchPosts({ commit, dispatch }, { i18n, filter }) {
const client = this.app.apolloProvider.defaultClient
const {
data: { Post },
} = await client.query({
query: gql`
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
Post(filter: $filter, first: $first, offset: $offset) {
id
title
contentExcerpt
createdAt
disabled
deleted
slug
image
author {
id
avatar
slug
name
disabled
deleted
contributionsCount
shoutedCount
commentedCount
followedByCount
followedByCurrentUser
location {
name: name${i18n.locale().toUpperCase()}
}
badges {
id
icon
}
}
categories {
id
name
icon
}
shoutedCount
}
}`,
variables: {
filter,
first: 12,
offset: 0,
},
})
commit('SET_POSTS', Post)
return Post
},
}

View File

@ -742,6 +742,14 @@
js-levenshtein "^1.1.3" js-levenshtein "^1.1.3"
semver "^5.5.0" semver "^5.5.0"
"@babel/runtime-corejs2@^7.2.0":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.5.5.tgz#c3214c08ef20341af4187f1c9fbdc357fbec96b2"
integrity sha512-FYATQVR00NSNi7mUfpPDp7E8RYMXDuO8gaix7u/w3GekfUinKgX1AcTxs7SoiEmoEW9mbpjrwqWSW6zCmw5h8A==
dependencies:
core-js "^2.6.5"
regenerator-runtime "^0.13.2"
"@babel/runtime@7.3.4": "@babel/runtime@7.3.4":
version "7.3.4" version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
@ -15734,6 +15742,13 @@ xml-name-validator@^3.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
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.0, xtend@~4.0.1: xtend@^4.0.0, xtend@~4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"