Merged master in and updated what had to be updated

This commit is contained in:
Grzegorz Leoniec 2019-03-08 20:50:58 +01:00
parent d16a1f62ff
commit 5c91962808
No known key found for this signature in database
GPG Key ID: 3AA43686D4EB1377
18 changed files with 1760 additions and 0 deletions

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Human-Connection gGmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,18 @@
version: "3.7"
services:
neo4j:
environment:
- NEO4J_AUTH=none
ports:
- 7687:7687
- 7474:7474
backend:
ports:
- 4001:4001
- 4123:4123
image: humanconnection/nitro-backend:builder
build:
context: .
target: builder
command: yarn run test:cypress

BIN
humanconnection.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

29
src/jwt/decode.js Normal file
View File

@ -0,0 +1,29 @@
import jwt from 'jsonwebtoken'
export default async (driver, authorizationHeader) => {
if (!authorizationHeader) return null
const token = authorizationHeader.replace('Bearer ', '')
let id = null
try {
const decoded = await jwt.verify(token, process.env.JWT_SECRET)
id = decoded.sub
} catch {
return null
}
const session = driver.session()
const query = `
MATCH (user:User {id: {id} })
RETURN user {.id, .slug, .name, .avatar, .email, .role} as user
LIMIT 1
`
const result = await session.run(query, { id })
session.close()
const [currentUser] = await result.records.map((record) => {
return record.get('user')
})
if (!currentUser) return null
return {
token,
...currentUser
}
}

17
src/jwt/encode.js Normal file
View File

@ -0,0 +1,17 @@
import jwt from 'jsonwebtoken'
import ms from 'ms'
// Generate an Access Token for the given User ID
export default function encode (user) {
const token = jwt.sign(user, process.env.JWT_SECRET, {
expiresIn: ms('1d'),
issuer: process.env.GRAPHQL_URI,
audience: process.env.CLIENT_URI,
subject: user.id.toString()
})
// jwt.verifySignature(token, process.env.JWT_SECRET, (err, data) => {
// console.log('token verification:', err, data)
// })
return token
}

View File

@ -0,0 +1,130 @@
import Factory from '../seed/factories'
import { host, login } from '../jest/helpers'
import { GraphQLClient } from 'graphql-request'
const factory = Factory()
let client
let query
let action
beforeEach(async () => {
await Promise.all([
factory.create('User', { role: 'user', email: 'user@example.org', password: '1234' }),
factory.create('User', { id: 'm1', role: 'moderator', email: 'moderator@example.org', password: '1234' })
])
await factory.authenticateAs({ email: 'user@example.org', password: '1234' })
await Promise.all([
factory.create('Post', { title: 'Deleted post', deleted: true }),
factory.create('Post', { id: 'p2', title: 'Disabled post', deleted: false }),
factory.create('Post', { title: 'Publicly visible post', deleted: false })
])
const moderatorFactory = Factory()
await moderatorFactory.authenticateAs({ email: 'moderator@example.org', password: '1234' })
const disableMutation = `
mutation {
disable(resource: {
id: "p2"
type: contribution
})
}
`
await moderatorFactory.mutate(disableMutation)
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('softDeleteMiddleware', () => {
describe('Post', () => {
action = () => {
return client.request(query)
}
beforeEach(() => {
query = '{ Post { title } }'
})
describe('as user', () => {
beforeEach(async () => {
const headers = await login({ email: 'user@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('hides deleted or disabled posts', async () => {
const expected = { Post: [{ title: 'Publicly visible post' }] }
await expect(action()).resolves.toEqual(expected)
})
})
describe('as moderator', () => {
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('hides deleted or disabled posts', async () => {
const expected = { Post: [{ title: 'Publicly visible post' }] }
await expect(action()).resolves.toEqual(expected)
})
})
describe('filter (deleted: true)', () => {
beforeEach(() => {
query = '{ Post(deleted: true) { title } }'
})
describe('as user', () => {
beforeEach(async () => {
const headers = await login({ email: 'user@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('throws authorisation error', async () => {
await expect(action()).rejects.toThrow('Not Authorised!')
})
})
describe('as moderator', () => {
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('shows deleted posts', async () => {
const expected = { Post: [{ title: 'Deleted post' }] }
await expect(action()).resolves.toEqual(expected)
})
})
})
describe('filter (disabled: true)', () => {
beforeEach(() => {
query = '{ Post(disabled: true) { title } }'
})
describe('as user', () => {
beforeEach(async () => {
const headers = await login({ email: 'user@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('throws authorisation error', async () => {
await expect(action()).rejects.toThrow('Not Authorised!')
})
})
describe('as moderator', () => {
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('shows disabled posts', async () => {
const expected = { Post: [{ title: 'Disabled post' }] }
await expect(action()).resolves.toEqual(expected)
})
})
})
})
})

View File

@ -0,0 +1,223 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
describe('badges', () => {
beforeEach(async () => {
await factory.create('User', {
email: 'user@example.org',
role: 'user',
password: '1234'
})
await factory.create('User', {
id: 'u2',
role: 'moderator',
email: 'moderator@example.org'
})
await factory.create('User', {
id: 'u3',
role: 'admin',
email: 'admin@example.org'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('CreateBadge', () => {
const variables = {
id: 'b1',
key: 'indiegogo_en_racoon',
type: 'crowdfunding',
status: 'permanent',
icon: '/img/badges/indiegogo_en_racoon.svg'
}
const mutation = `
mutation(
$id: ID
$key: String!
$type: BadgeTypeEnum!
$status: BadgeStatusEnum!
$icon: String!
) {
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
id,
key,
type,
status,
icon
}
}
`
describe('unauthenticated', () => {
let client
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
})
})
describe('authenticated admin', () => {
let client
beforeEach(async () => {
const headers = await login({ email: 'admin@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('creates a badge', async () => {
const expected = {
CreateBadge: {
icon: '/img/badges/indiegogo_en_racoon.svg',
id: 'b1',
key: 'indiegogo_en_racoon',
status: 'permanent',
type: 'crowdfunding'
}
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
})
describe('authenticated moderator', () => {
let client
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('throws authorization error', async () => {
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
})
})
})
describe('UpdateBadge', () => {
beforeEach(async () => {
await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
await factory.create('Badge', { id: 'b1' })
})
const variables = {
id: 'b1',
key: 'whatever'
}
const mutation = `
mutation($id: ID!, $key: String!) {
UpdateBadge(id: $id, key: $key) {
id
key
}
}
`
describe('unauthenticated', () => {
let client
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
})
})
describe('authenticated moderator', () => {
let client
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('throws authorization error', async () => {
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
})
})
describe('authenticated admin', () => {
let client
beforeEach(async () => {
const headers = await login({ email: 'admin@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('updates a badge', async () => {
const expected = {
UpdateBadge: {
id: 'b1',
key: 'whatever'
}
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
})
})
describe('DeleteBadge', () => {
beforeEach(async () => {
await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
await factory.create('Badge', { id: 'b1' })
})
const variables = {
id: 'b1'
}
const mutation = `
mutation($id: ID!) {
DeleteBadge(id: $id) {
id
}
}
`
describe('unauthenticated', () => {
let client
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
})
})
describe('authenticated moderator', () => {
let client
beforeEach(async () => {
const headers = await login({ email: 'moderator@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('throws authorization error', async () => {
await expect(
client.request(mutation, variables)
).rejects.toThrow('Not Authorised')
})
})
describe('authenticated admin', () => {
let client
beforeEach(async () => {
const headers = await login({ email: 'admin@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('deletes a badge', async () => {
const expected = {
DeleteBadge: {
id: 'b1'
}
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
})
})
})

View File

@ -0,0 +1,115 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
let clientUser1
const mutationFollowUser = (id) => `
mutation {
follow(id: "${id}", type: User)
}
`
const mutationUnfollowUser = (id) => `
mutation {
unfollow(id: "${id}", type: User)
}
`
beforeEach(async () => {
await factory.create('User', {
id: 'u1',
email: 'test@example.org',
password: '1234'
})
await factory.create('User', {
id: 'u2',
email: 'test2@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('follow ', () => {
describe('(un)follow user', () => {
let headersUser1
beforeEach(async () => {
headersUser1 = await login({ email: 'test@example.org', password: '1234' })
clientUser1 = new GraphQLClient(host, { headers: headersUser1 })
})
it('I can follow another user', async () => {
const res = await clientUser1.request(
mutationFollowUser('u2')
)
const expected = {
follow: true
}
expect(res).toMatchObject(expected)
const { User } = await clientUser1.request(`{
User(id: "u2") {
followedBy { id }
followedByCurrentUser
}
}`)
const expected2 = {
followedBy: [
{ id: 'u1' }
],
followedByCurrentUser: true
}
expect(User[0]).toMatchObject(expected2)
})
it('I can unfollow a user', async () => {
// follow
await clientUser1.request(
mutationFollowUser('u2')
)
const expected = {
unfollow: true
}
// unfollow
const res = await clientUser1.request(mutationUnfollowUser('u2'))
expect(res).toMatchObject(expected)
const { User } = await clientUser1.request(`{
User(id: "u2") {
followedBy { id }
followedByCurrentUser
}
}`)
const expected2 = {
followedBy: [],
followedByCurrentUser: false
}
expect(User[0]).toMatchObject(expected2)
})
it('I can`t follow myself', async () => {
const res = await clientUser1.request(
mutationFollowUser('u1')
)
const expected = {
follow: false
}
expect(res).toMatchObject(expected)
const { User } = await clientUser1.request(`{
User(id: "u1") {
followedBy { id }
followedByCurrentUser
}
}`)
const expected2 = {
followedBy: [],
followedByCurrentUser: false
}
expect(User[0]).toMatchObject(expected2)
})
})
})

View File

@ -0,0 +1,30 @@
export default {
Mutation: {
disable: async (object, params, { user, driver }) => {
const { resource: { id } } = params
const { id: userId } = user
const cypher = `
MATCH (u:User {id: $userId})
MATCH (r {id: $id})
SET r.disabled = true
MERGE (r)<-[:DISABLED]-(u)
`
const session = driver.session()
const res = await session.run(cypher, { id, userId })
session.close()
return Boolean(res)
},
enable: async (object, params, { user, driver }) => {
const { resource: { id } } = params
const cypher = `
MATCH (r {id: $id})<-[d:DISABLED]-()
SET r.disabled = false
DELETE d
`
const session = driver.session()
const res = await session.run(cypher, { id })
session.close()
return Boolean(res)
}
}
}

View File

@ -0,0 +1,370 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
let client
const setupAuthenticateClient = (params) => {
const authenticateClient = async () => {
await factory.create('User', params)
const headers = await login(params)
client = new GraphQLClient(host, { headers })
}
return authenticateClient
}
let setup
const runSetup = async () => {
await setup.createResource()
await setup.authenticateClient()
}
beforeEach(() => {
setup = {
createResource: () => {
},
authenticateClient: () => {
client = new GraphQLClient(host)
}
}
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('disable', () => {
const mutation = `
mutation($id: ID!, $type: ResourceEnum!) {
disable(resource: { id: $id, type: $type })
}
`
let variables
beforeEach(() => {
// our defaul set of variables
variables = {
id: 'blabla',
type: 'contribution'
}
})
const action = async () => {
return client.request(mutation, variables)
}
it('throws authorization error', async () => {
await runSetup()
await expect(action()).rejects.toThrow('Not Authorised')
})
describe('authenticated', () => {
beforeEach(() => {
setup.authenticateClient = setupAuthenticateClient({
email: 'user@example.org',
password: '1234'
})
})
it('throws authorization error', async () => {
await runSetup()
await expect(action()).rejects.toThrow('Not Authorised')
})
describe('as moderator', () => {
beforeEach(() => {
setup.authenticateClient = setupAuthenticateClient({
id: 'u7',
email: 'moderator@example.org',
password: '1234',
role: 'moderator'
})
})
describe('on a comment', () => {
beforeEach(async () => {
variables = {
id: 'c47',
type: 'comment'
}
setup.createResource = async () => {
await factory.create('User', { id: 'u45', email: 'commenter@example.org', password: '1234' })
await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' })
await Promise.all([
factory.create('Post', { id: 'p3' }),
factory.create('Comment', { id: 'c47' })
])
await Promise.all([
factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' }),
factory.relate('Comment', 'Post', { from: 'c47', to: 'p3' })
])
}
})
it('returns true', async () => {
const expected = { disable: true }
await runSetup()
await expect(action()).resolves.toEqual(expected)
})
it('changes .disabledBy', async () => {
const before = { Comment: [{ id: 'c47', disabledBy: null }] }
const expected = { Comment: [{ id: 'c47', disabledBy: { id: 'u7' } }] }
await runSetup()
await expect(client.request(
'{ Comment { id, disabledBy { id } } }'
)).resolves.toEqual(before)
await action()
await expect(client.request(
'{ Comment(disabled: true) { id, disabledBy { id } } }'
)).resolves.toEqual(expected)
})
it('updates .disabled on comment', async () => {
const before = { Comment: [ { id: 'c47', disabled: false } ] }
const expected = { Comment: [ { id: 'c47', disabled: true } ] }
await runSetup()
await expect(client.request(
'{ Comment { id disabled } }'
)).resolves.toEqual(before)
await action()
await expect(client.request(
'{ Comment(disabled: true) { id disabled } }'
)).resolves.toEqual(expected)
})
})
describe('on a post', () => {
beforeEach(async () => {
variables = {
id: 'p9',
type: 'contribution'
}
setup.createResource = async () => {
await factory.create('User', { email: 'author@example.org', password: '1234' })
await factory.authenticateAs({ email: 'author@example.org', password: '1234' })
await factory.create('Post', {
id: 'p9' // that's the ID we will look for
})
}
})
it('returns true', async () => {
const expected = { disable: true }
await runSetup()
await expect(action()).resolves.toEqual(expected)
})
it('changes .disabledBy', async () => {
const before = { Post: [{ id: 'p9', disabledBy: null }] }
const expected = { Post: [{ id: 'p9', disabledBy: { id: 'u7' } }] }
await runSetup()
await expect(client.request(
'{ Post { id, disabledBy { id } } }'
)).resolves.toEqual(before)
await action()
await expect(client.request(
'{ Post(disabled: true) { id, disabledBy { id } } }'
)).resolves.toEqual(expected)
})
it('updates .disabled on post', async () => {
const before = { Post: [ { id: 'p9', disabled: false } ] }
const expected = { Post: [ { id: 'p9', disabled: true } ] }
await runSetup()
await expect(client.request(
'{ Post { id disabled } }'
)).resolves.toEqual(before)
await action()
await expect(client.request(
'{ Post(disabled: true) { id disabled } }'
)).resolves.toEqual(expected)
})
})
})
})
})
describe('enable', () => {
const mutation = `
mutation($id: ID!, $type: ResourceEnum!) {
enable(resource: { id: $id, type: $type })
}
`
let variables
const action = async () => {
return client.request(mutation, variables)
}
beforeEach(() => {
// our defaul set of variables
variables = {
id: 'blabla',
type: 'contribution'
}
})
it('throws authorization error', async () => {
await runSetup()
await expect(action()).rejects.toThrow('Not Authorised')
})
describe('authenticated', () => {
beforeEach(() => {
setup.authenticateClient = setupAuthenticateClient({
email: 'user@example.org',
password: '1234'
})
})
it('throws authorization error', async () => {
await runSetup()
await expect(action()).rejects.toThrow('Not Authorised')
})
describe('as moderator', () => {
beforeEach(async () => {
setup.authenticateClient = setupAuthenticateClient({
role: 'moderator',
email: 'someUser@example.org',
password: '1234'
})
})
describe('on a comment', () => {
beforeEach(async () => {
variables = {
id: 'c456',
type: 'comment'
}
setup.createResource = async () => {
await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' })
await factory.authenticateAs({ email: 'author@example.org', password: '1234' })
await Promise.all([
factory.create('Post', { id: 'p9' }),
factory.create('Comment', { id: 'c456' })
])
await Promise.all([
factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' }),
factory.relate('Comment', 'Post', { from: 'c456', to: 'p9' })
])
const disableMutation = `
mutation {
disable(resource: {
id: "c456"
type: comment
})
}
`
await factory.mutate(disableMutation) // that's we want to delete
}
})
it('returns true', async () => {
const expected = { enable: true }
await runSetup()
await expect(action()).resolves.toEqual(expected)
})
it('changes .disabledBy', async () => {
const before = { Comment: [{ id: 'c456', disabledBy: { id: 'u123' } }] }
const expected = { Comment: [{ id: 'c456', disabledBy: null }] }
await runSetup()
await expect(client.request(
'{ Comment(disabled: true) { id, disabledBy { id } } }'
)).resolves.toEqual(before)
await action()
await expect(client.request(
'{ Comment { id, disabledBy { id } } }'
)).resolves.toEqual(expected)
})
it('updates .disabled on post', async () => {
const before = { Comment: [ { id: 'c456', disabled: true } ] }
const expected = { Comment: [ { id: 'c456', disabled: false } ] }
await runSetup()
await expect(client.request(
'{ Comment(disabled: true) { id disabled } }'
)).resolves.toEqual(before)
await action() // this updates .disabled
await expect(client.request(
'{ Comment { id disabled } }'
)).resolves.toEqual(expected)
})
})
describe('on a post', () => {
beforeEach(async () => {
variables = {
id: 'p9',
type: 'contribution'
}
setup.createResource = async () => {
await factory.create('User', { id: 'u123', email: 'author@example.org', password: '1234' })
await factory.authenticateAs({ email: 'author@example.org', password: '1234' })
await factory.create('Post', {
id: 'p9' // that's the ID we will look for
})
const disableMutation = `
mutation {
disable(resource: {
id: "p9"
type: contribution
})
}
`
await factory.mutate(disableMutation) // that's we want to delete
}
})
it('returns true', async () => {
const expected = { enable: true }
await runSetup()
await expect(action()).resolves.toEqual(expected)
})
it('changes .disabledBy', async () => {
const before = { Post: [{ id: 'p9', disabledBy: { id: 'u123' } }] }
const expected = { Post: [{ id: 'p9', disabledBy: null }] }
await runSetup()
await expect(client.request(
'{ Post(disabled: true) { id, disabledBy { id } } }'
)).resolves.toEqual(before)
await action()
await expect(client.request(
'{ Post { id, disabledBy { id } } }'
)).resolves.toEqual(expected)
})
it('updates .disabled on post', async () => {
const before = { Post: [ { id: 'p9', disabled: true } ] }
const expected = { Post: [ { id: 'p9', disabled: false } ] }
await runSetup()
await expect(client.request(
'{ Post(disabled: true) { id disabled } }'
)).resolves.toEqual(before)
await action() // this updates .disabled
await expect(client.request(
'{ Post { id disabled } }'
)).resolves.toEqual(expected)
})
})
})
})
})

63
src/resolvers/posts.js Normal file
View File

@ -0,0 +1,63 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
import { activityPub } from '../activitypub/ActivityPub'
import uuid from 'uuid/v4'
import as from 'activitystrea.ms'
/*
import as from 'activitystrea.ms'
import request from 'request'
*/
const debug = require('debug')('backend:schema')
export default {
Mutation: {
CreatePost: async (object, params, context, resolveInfo) => {
params.activityId = uuid()
const result = await neo4jgraphql(object, params, context, resolveInfo, false)
const session = context.driver.session()
const author = await session.run(
'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' +
'MERGE (post)<-[:WROTE]-(author) ' +
'RETURN author', {
userId: context.user.id,
postId: result.id
})
debug(`actorId = ${author.records[0]._fields[0].properties.actorId}`)
if (Array.isArray(author.records) && author.records.length > 0) {
const actorId = author.records[0]._fields[0].properties.actorId
const createActivity = await new Promise((resolve, reject) => {
as.create()
.id(`${actorId}/status/${params.activityId}`)
.actor(`${actorId}`)
.object(
as.article()
.id(`${actorId}/status/${result.id}`)
.content(result.content)
.to('https://www.w3.org/ns/activitystreams#Public')
.publishedNow()
.attributedTo(`${actorId}`)
).prettyWrite((err, doc) => {
if (err) {
reject(err)
} else {
debug(doc)
const parsedDoc = JSON.parse(doc)
parsedDoc.send = true
resolve(JSON.stringify(parsedDoc))
}
})
})
try {
await activityPub.sendActivity(createActivity)
} catch (e) {
debug(`error sending post activity = ${JSON.stringify(e, null, 2)}`)
}
}
session.close()
return result
}
}
}

202
src/resolvers/posts.spec.js Normal file
View File

@ -0,0 +1,202 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
let client
beforeEach(async () => {
await factory.create('User', {
email: 'test@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('CreatePost', () => {
const mutation = `
mutation {
CreatePost(title: "I am a title", content: "Some content") {
title
content
slug
disabled
deleted
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(mutation)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('creates a post', async () => {
const expected = {
CreatePost: {
title: 'I am a title',
content: 'Some content'
}
}
await expect(client.request(mutation)).resolves.toMatchObject(expected)
})
it('assigns the authenticated user as author', async () => {
await client.request(mutation)
const { User } = await client.request(`{
User(email:"test@example.org") {
contributions {
title
}
}
}`, { headers })
expect(User).toEqual([ { contributions: [ { title: 'I am a title' } ] } ])
})
describe('disabled and deleted', () => {
it('initially false', async () => {
const expected = { CreatePost: { disabled: false, deleted: false } }
await expect(client.request(mutation)).resolves.toMatchObject(expected)
})
})
})
})
describe('UpdatePost', () => {
const mutation = `
mutation($id: ID!, $content: String) {
UpdatePost(id: $id, content: $content) {
id
content
}
}
`
let variables = {
id: 'p1',
content: 'New content'
}
beforeEach(async () => {
const asAuthor = Factory()
await asAuthor.create('User', {
email: 'author@example.org',
password: '1234'
})
await asAuthor.authenticateAs({
email: 'author@example.org',
password: '1234'
})
await asAuthor.create('Post', {
id: 'p1',
content: 'Old content'
})
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated but not the author', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('throws authorization error', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated as author', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'author@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('updates a post', async () => {
const expected = { UpdatePost: { id: 'p1', content: 'New content' } }
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
})
})
describe('DeletePost', () => {
const mutation = `
mutation($id: ID!) {
DeletePost(id: $id) {
id
content
}
}
`
let variables = {
id: 'p1'
}
beforeEach(async () => {
const asAuthor = Factory()
await asAuthor.create('User', {
email: 'author@example.org',
password: '1234'
})
await asAuthor.authenticateAs({
email: 'author@example.org',
password: '1234'
})
await asAuthor.create('Post', {
id: 'p1',
content: 'To be deleted'
})
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated but not the author', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('throws authorization error', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated as author', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'author@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('deletes a post', async () => {
const expected = { DeletePost: { id: 'p1', content: 'To be deleted' } }
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
})
})

51
src/resolvers/reports.js Normal file
View File

@ -0,0 +1,51 @@
import uuid from 'uuid/v4'
export default {
Mutation: {
report: async (parent, { resource, description }, { driver, req, user }, resolveInfo) => {
const contextId = uuid()
const session = driver.session()
const data = {
id: contextId,
type: resource.type,
createdAt: (new Date()).toISOString(),
description: resource.description
}
await session.run(
'CREATE (r:Report $report) ' +
'RETURN r.id, r.type, r.description', {
report: data
}
)
let contentType
switch (resource.type) {
case 'post':
case 'contribution':
contentType = 'Post'
break
case 'comment':
contentType = 'Comment'
break
case 'user':
contentType = 'User'
break
}
await session.run(
`MATCH (author:User {id: $userId}), (context:${contentType} {id: $resourceId}), (report:Report {id: $contextId}) ` +
'MERGE (report)<-[:REPORTED]-(author) ' +
'MERGE (context)<-[:REPORTED]-(report) ' +
'RETURN context', {
resourceId: resource.id,
userId: user.id,
contextId: contextId
}
)
session.close()
// TODO: output Report compatible object
return data
}
}
}

View File

@ -0,0 +1,68 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
describe('report', () => {
beforeEach(async () => {
await factory.create('User', {
email: 'test@example.org',
password: '1234'
})
await factory.create('User', {
id: 'u2',
name: 'abusive-user',
role: 'user',
email: 'abusive-user@example.org'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('unauthenticated', () => {
let client
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(
client.request(`mutation {
report(
description: "I don't like this user",
resource: {
id: "u2",
type: user
}
) { id, createdAt }
}`)
).rejects.toThrow('Not Authorised')
})
describe('authenticated', () => {
let headers
let response
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
response = await client.request(`mutation {
report(
description: "I don't like this user",
resource: {
id: "u2",
type: user
}
) { id, createdAt }
}`,
{ headers }
)
})
it('creates a report', () => {
let { id, createdAt } = response.report
expect(response).toEqual({
report: { id, createdAt }
})
})
})
})
})

126
src/resolvers/shout.spec.js Normal file
View File

@ -0,0 +1,126 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers'
const factory = Factory()
let clientUser1, clientUser2
const mutationShoutPost = (id) => `
mutation {
shout(id: "${id}", type: Post)
}
`
const mutationUnshoutPost = (id) => `
mutation {
unshout(id: "${id}", type: Post)
}
`
beforeEach(async () => {
await factory.create('User', {
id: 'u1',
email: 'test@example.org',
password: '1234'
})
await factory.create('User', {
id: 'u2',
email: 'test2@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('shout ', () => {
describe('(un)shout foreign post', () => {
let headersUser1, headersUser2
beforeEach(async () => {
headersUser1 = await login({ email: 'test@example.org', password: '1234' })
headersUser2 = await login({ email: 'test2@example.org', password: '1234' })
clientUser1 = new GraphQLClient(host, { headers: headersUser1 })
clientUser2 = new GraphQLClient(host, { headers: headersUser2 })
await clientUser1.request(`
mutation {
CreatePost(id: "p1", title: "Post Title 1", content: "Some Post Content 1") {
id
title
}
}
`)
await clientUser2.request(`
mutation {
CreatePost(id: "p2", title: "Post Title 2", content: "Some Post Content 2") {
id
title
}
}
`)
})
it('I shout a post of another user', async () => {
const res = await clientUser1.request(
mutationShoutPost('p2')
)
const expected = {
shout: true
}
expect(res).toMatchObject(expected)
const { Post } = await clientUser1.request(`{
Post(id: "p2") {
shoutedByCurrentUser
}
}`)
const expected2 = {
shoutedByCurrentUser: true
}
expect(Post[0]).toMatchObject(expected2)
})
it('I unshout a post of another user', async () => {
// shout
await clientUser1.request(
mutationShoutPost('p2')
)
const expected = {
unshout: true
}
// unshout
const res = await clientUser1.request(mutationUnshoutPost('p2'))
expect(res).toMatchObject(expected)
const { Post } = await clientUser1.request(`{
Post(id: "p2") {
shoutedByCurrentUser
}
}`)
const expected2 = {
shoutedByCurrentUser: false
}
expect(Post[0]).toMatchObject(expected2)
})
it('I can`t shout my own post', async () => {
const res = await clientUser1.request(
mutationShoutPost('p1')
)
const expected = {
shout: false
}
expect(res).toMatchObject(expected)
const { Post } = await clientUser1.request(`{
Post(id: "p1") {
shoutedByCurrentUser
}
}`)
const expected2 = {
shoutedByCurrentUser: false
}
expect(Post[0]).toMatchObject(expected2)
})
})
})

View File

@ -0,0 +1,67 @@
export const query = (cypher, session) => {
return new Promise((resolve, reject) => {
let data = []
session
.run(cypher)
.subscribe({
onNext: function (record) {
let item = {}
record.keys.forEach(key => {
item[key] = record.get(key)
})
data.push(item)
},
onCompleted: function () {
session.close()
resolve(data)
},
onError: function (error) {
reject(error)
}
})
})
}
const queryOne = (cypher, session) => {
return new Promise((resolve, reject) => {
query(cypher, session)
.then(res => {
resolve(res.length ? res.pop() : {})
})
.catch(err => {
reject(err)
})
})
}
export default {
Query: {
statistics: async (parent, args, { driver, user }) => {
return new Promise(async (resolve) => {
const session = driver.session()
const queries = {
countUsers: 'MATCH (r:User) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countUsers',
countPosts: 'MATCH (r:Post) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countPosts',
countComments: 'MATCH (r:Comment) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countComments',
countNotifications: 'MATCH (r:Notification) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countNotifications',
countOrganizations: 'MATCH (r:Organization) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countOrganizations',
countProjects: 'MATCH (r:Project) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countProjects',
countInvites: 'MATCH (r:Invite) WHERE r.wasUsed <> true OR NOT exists(r.wasUsed) RETURN COUNT(r) AS countInvites',
countFollows: 'MATCH (:User)-[r:FOLLOWS]->(:User) RETURN COUNT(r) AS countFollows',
countShouts: 'MATCH (:User)-[r:SHOUTED]->(:Post) RETURN COUNT(r) AS countShouts'
}
let data = {
countUsers: (await queryOne(queries.countUsers, session)).countUsers.low,
countPosts: (await queryOne(queries.countPosts, session)).countPosts.low,
countComments: (await queryOne(queries.countComments, session)).countComments.low,
countNotifications: (await queryOne(queries.countNotifications, session)).countNotifications.low,
countOrganizations: (await queryOne(queries.countOrganizations, session)).countOrganizations.low,
countProjects: (await queryOne(queries.countProjects, session)).countProjects.low,
countInvites: (await queryOne(queries.countInvites, session)).countInvites.low,
countFollows: (await queryOne(queries.countFollows, session)).countFollows.low,
countShouts: (await queryOne(queries.countShouts, session)).countShouts.low
}
resolve(data)
})
}
}
}

View File

@ -0,0 +1,51 @@
import encode from '../jwt/encode'
import bcrypt from 'bcryptjs'
import { AuthenticationError } from 'apollo-server'
import { neo4jgraphql } from 'neo4j-graphql-js'
export default {
Query: {
isLoggedIn: (parent, args, { driver, user }) => {
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)
}
},
Mutation: {
signup: async (parent, { email, password }, { req }) => {
// if (data[email]) {
// throw new Error('Another User with same email exists.')
// }
// data[email] = {
// password: await bcrypt.hashSync(password, 10),
// }
return true
},
login: async (parent, { email, password }, { driver, req, user }) => {
// if (user && user.id) {
// throw new Error('Already logged in.')
// }
const session = driver.session()
return session.run(
'MATCH (user:User {email: $userEmail}) ' +
'RETURN user {.id, .slug, .name, .avatar, .email, .password, .role} as user LIMIT 1', {
userEmail: email
})
.then(async (result) => {
session.close()
const [currentUser] = await result.records.map(function (record) {
return record.get('user')
})
if (currentUser && await bcrypt.compareSync(password, currentUser.password)) {
delete currentUser.password
return encode(currentUser)
} else throw new AuthenticationError('Incorrect email address or password.')
})
}
}
}

View File

@ -0,0 +1,179 @@
import Factory from '../seed/factories'
import { GraphQLClient, request } from 'graphql-request'
import jwt from 'jsonwebtoken'
import { host, login } from '../jest/helpers'
const factory = Factory()
// here is the decoded JWT token:
// {
// role: 'user',
// locationName: null,
// name: 'Jenny Rostock',
// about: null,
// avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sasha_shestakov/128.jpg',
// id: 'u3',
// email: 'user@example.org',
// slug: 'jenny-rostock',
// iat: 1550846680,
// exp: 1637246680,
// aud: 'http://localhost:3000',
// iss: 'http://localhost:4000',
// sub: 'u3'
// }
const jennyRostocksHeaders = { authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc' }
beforeEach(async () => {
await factory.create('User', {
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/seyedhossein1/128.jpg',
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
name: 'Matilde Hermiston',
slug: 'matilde-hermiston',
role: 'user',
email: 'test@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('isLoggedIn', () => {
const query = '{ isLoggedIn }'
describe('unauthenticated', () => {
it('returns false', async () => {
await expect(request(host, query)).resolves.toEqual({ isLoggedIn: false })
})
})
describe('with malformed JWT Bearer token', () => {
const headers = { authorization: 'blah' }
const client = new GraphQLClient(host, { headers })
it('returns false', async () => {
await expect(client.request(query)).resolves.toEqual({ isLoggedIn: false })
})
})
describe('with valid JWT Bearer token', () => {
const client = new GraphQLClient(host, { headers: jennyRostocksHeaders })
it('returns false', async () => {
await expect(client.request(query)).resolves.toEqual({ isLoggedIn: false })
})
describe('and a corresponding user in the database', () => {
it('returns true', async () => {
// see the decoded token above
await factory.create('User', { id: 'u3' })
await expect(client.request(query)).resolves.toEqual({ isLoggedIn: true })
})
})
})
})
describe('currentUser', () => {
const query = `{
currentUser {
id
slug
name
avatar
email
role
}
}`
describe('unauthenticated', () => {
it('returns null', async () => {
const expected = { currentUser: null }
await expect(request(host, query)).resolves.toEqual(expected)
})
})
describe('with valid JWT Bearer Token', () => {
let client
let headers
describe('but no corresponding user in the database', () => {
beforeEach(async () => {
client = new GraphQLClient(host, { headers: jennyRostocksHeaders })
})
it('returns null', async () => {
const expected = { currentUser: null }
await expect(client.request(query)).resolves.toEqual(expected)
})
})
describe('and corresponding user in the database', () => {
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
})
it('returns the whole user object', async () => {
const expected = {
currentUser: {
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/seyedhossein1/128.jpg',
email: 'test@example.org',
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
name: 'Matilde Hermiston',
slug: 'matilde-hermiston',
role: 'user'
}
}
await expect(client.request(query)).resolves.toEqual(expected)
})
})
})
})
describe('login', () => {
const mutation = (params) => {
const { email, password } = params
return `
mutation {
login(email:"${email}", password:"${password}")
}`
}
describe('ask for a `token`', () => {
describe('with valid email/password combination', () => {
it('responds with a JWT token', async () => {
const data = await request(host, mutation({
email: 'test@example.org',
password: '1234'
}))
const token = data.login
jwt.verify(token, process.env.JWT_SECRET, (err, data) => {
expect(data.email).toEqual('test@example.org')
expect(err).toBeNull()
})
})
})
describe('with a valid email but incorrect password', () => {
it('responds with "Incorrect email address or password."', async () => {
await expect(
request(host, mutation({
email: 'test@example.org',
password: 'wrong'
}))
).rejects.toThrow('Incorrect email address or password.')
})
})
describe('with a non-existing email', () => {
it('responds with "Incorrect email address or password."', async () => {
await expect(
request(host, mutation({
email: 'non-existent@example.org',
password: 'wrong'
}))
).rejects.toThrow('Incorrect email address or password.')
})
})
})
})