Updates to get in line with master

This commit is contained in:
Grzegorz Leoniec 2019-03-08 20:51:43 +01:00
parent 5c91962808
commit 45cf16d07d
No known key found for this signature in database
GPG Key ID: 3AA43686D4EB1377
23 changed files with 1192 additions and 1185 deletions

View File

@ -8,3 +8,5 @@ MOCK=false
JWT_SECRET="b/&&7b78BF&fv/Vd"
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"
PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"

3
.gitignore vendored
View File

@ -8,3 +8,6 @@ coverage.lcov
.nyc_output/
public/uploads/*
!.gitkeep
# Apple macOS folder attribute file
.DS_Store

View File

@ -1,5 +1,12 @@
# Human-Connection - NITRO Backend
[![Build Status](https://travis-ci.com/Human-Connection/Nitro-Backend.svg?branch=master)](https://travis-ci.com/Human-Connection/Nitro-Backend)
<p align="center">
<a href="https://human-connection.org"><img align="center" src="humanconnection.png" height="200" alt="Human Connection" /></a>
</p>
# NITRO Backend
[![Build Status](https://img.shields.io/travis/com/Human-Connection/Nitro-Backend/master.svg)](https://travis-ci.com/Human-Connection/Nitro-Backend)
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_shield)
[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/6ub73U3)
> This Prototype tries to resolve the biggest hurdle of connecting
> our services together. This is not possible in a sane way using
@ -158,9 +165,13 @@ npm run test:cucumber
- [x] add jwt authentication
- [ ] get directives working correctly (@toLower, @auth, @role, etc.)
- [ ] check if search is working
- [x] check if search is working
- [x] check if sorting is working
- [x] check if pagination is working
- [ ] check if upload is working (using graphql-yoga?)
- [x] evaluate middleware
- [ ] ignore Posts and Comments by blacklisted Users
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FHuman-Connection%2FNitro-Backend?ref=badge_large)

View File

@ -8,11 +8,7 @@ services:
- 7687:7687
- 7474:7474
backend:
ports:
- 4001:4001
- 4123:4123
image: humanconnection/nitro-backend:builder
build:
context: .
target: builder
command: yarn run test:cypress

View File

@ -9,9 +9,9 @@
"scripts": {
"build": "babel src/ -d dist/ --copy-files",
"start": "node dist/",
"dev": "nodemon --exec babel-node src/",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js",
"lint": "eslint src --config .eslintrc.js --fix",
"dev": "nodemon --exec babel-node src/ -e js,graphql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,graphql",
"lint": "eslint src --config .eslintrc.js",
"test": "nyc --reporter=text-lcov yarn test:jest",
"test:cypress": "run-p --race test:before:*",
"test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 babel-node src/ 2> /dev/null",
@ -36,19 +36,19 @@
]
},
"dependencies": {
"activitystrea.ms": "^2.1.3",
"apollo-cache-inmemory": "~1.4.3",
"apollo-client": "~2.4.13",
"apollo-link-context": "^1.0.14",
"apollo-link-http": "~1.5.11",
"apollo-server": "~2.4.2",
"activitystrea.ms": "~2.1.3",
"apollo-cache-inmemory": "~1.5.1",
"apollo-client": "~2.5.1",
"apollo-link-context": "~1.0.14",
"apollo-link-http": "~1.5.12",
"apollo-server": "~2.4.8",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.2",
"cors": "^2.8.5",
"cors": "~2.8.5",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.26",
"dotenv": "~6.2.0",
"express": "^4.16.4",
"express": "~4.16.4",
"faker": "~4.1.0",
"graphql": "~14.1.1",
"graphql-custom-directives": "~0.2.14",
@ -57,39 +57,37 @@
"graphql-shield": "~5.3.0",
"graphql-tag": "~2.10.1",
"graphql-yoga": "~1.17.4",
"helmet": "^3.15.1",
"helmet": "~3.15.1",
"jsonwebtoken": "~8.5.0",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.11",
"ms": "~2.1.1",
"neo4j-driver": "~1.7.2",
"neo4j-graphql-js": "~2.3.1",
"neo4j-driver": "~1.7.3",
"neo4j-graphql-js": "~2.4.1",
"node-fetch": "~2.3.0",
"npm-run-all": "~4.1.5",
"passport": "~0.4.0",
"passport-jwt": "~4.0.0",
"request": "^2.88.0",
"request": "~2.88.0",
"sanitize-html": "~1.20.0",
"slug": "~1.0.0",
"trunc-html": "~1.1.2",
"uuid": "^3.3.2",
"uuid": "~3.3.2",
"wait-on": "~3.2.0"
},
"devDependencies": {
"@babel/cli": "~7.2.3",
"@babel/core": "~7.3.3",
"@babel/core": "~7.3.4",
"@babel/node": "~7.2.2",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.3.1",
"@babel/preset-env": "~7.3.4",
"@babel/register": "~7.0.0",
"apollo-server-testing": "~2.4.2",
"apollo-server-testing": "~2.4.8",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.1",
"babel-jest": "~24.1.0",
"babel-jest": "~24.3.1",
"chai": "~4.2.0",
"cucumber": "^5.1.0",
"debug": "^4.1.1",
"eslint": "~5.13.0",
"cucumber": "~5.1.0",
"debug": "~4.1.1",
"eslint": "~5.15.1",
"eslint-config-standard": "~12.0.0",
"eslint-plugin-import": "~2.16.0",
"eslint-plugin-jest": "~22.3.0",
@ -97,7 +95,7 @@
"eslint-plugin-promise": "~4.0.1",
"eslint-plugin-standard": "~4.0.0",
"graphql-request": "~1.8.2",
"jest": "~24.1.0",
"jest": "~24.3.1",
"nodemon": "~1.18.10",
"nyc": "~13.3.0",
"supertest": "~3.4.2"

View File

@ -21,7 +21,7 @@ import fetch from 'node-fetch'
import { ApolloClient } from 'apollo-client'
import dotenv from 'dotenv'
import uuid from 'uuid'
import generateJwtToken from '../jwt/generateToken'
import encode from '../jwt/encode'
import { resolve } from 'path'
import trunc from 'trunc-html'
const debug = require('debug')('ea:nitro-datasource')
@ -41,7 +41,7 @@ export default class NitroDataSource {
const cache = new InMemoryCache()
const authLink = setContext((_, { headers }) => {
// generate the authentication token (maybe from env? Which user?)
const token = generateJwtToken({ name: 'ActivityPub', id: uuid() })
const token = encode({ name: 'ActivityPub', id: uuid() })
// return the headers to the context so httpLink can read them
return {
headers: {

View File

@ -1,219 +1,25 @@
import fs from 'fs'
import path from 'path'
import bcrypt from 'bcryptjs'
import generateJwt from './jwt/generateToken'
import uuid from 'uuid/v4'
import { fixUrl } from './middleware/fixImageUrlsMiddleware'
import { AuthenticationError } from 'apollo-server'
import { neo4jgraphql } from 'neo4j-graphql-js'
import { activityPub } from './activitypub/ActivityPub'
import as from 'activitystrea.ms'
/*
import as from 'activitystrea.ms'
import request from 'request'
*/
const debug = require('debug')('backend:schema')
import userManagement from './resolvers/user_management.js'
import statistics from './resolvers/statistics.js'
import reports from './resolvers/reports.js'
import posts from './resolvers/posts.js'
import moderation from './resolvers/moderation.js'
export const typeDefs =
fs.readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql'))
.toString('utf-8')
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 const resolvers = {
Query: {
isLoggedIn: (parent, args, { driver, user }) => {
return Boolean(user && user.id)
},
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)
})
}
// usersBySubstring: neo4jgraphql
...statistics.Query,
...userManagement.Query
},
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, .locationName, .about, .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
currentUser.avatar = fixUrl(currentUser.avatar)
return Object.assign(currentUser, {
token: generateJwt(currentUser)
})
} else throw new AuthenticationError('Incorrect email address or password.')
})
},
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
},
CreatePost: async (object, params, ctx, resolveInfo) => {
params.activityId = uuid()
const result = await neo4jgraphql(object, params, ctx, resolveInfo, false)
debug(`user = ${JSON.stringify(ctx.user, null, 2)}`)
const session = ctx.driver.session()
const author = await session.run(
'MATCH (author:User {slug: $slug}), (post:Post {id: $postId}) ' +
'MERGE (post)<-[:WROTE]-(author) ' +
'RETURN author', {
slug: ctx.user.slug,
postId: result.id
})
// debug(`result = ${JSON.stringify(author, null, 2)}`)
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)}`)
}
}
}
...userManagement.Mutation,
...reports.Mutation,
...moderation.Mutation,
...posts.Mutation
}
}

View File

@ -1,184 +0,0 @@
import Factory from './seed/factories'
import { GraphQLClient, request } from 'graphql-request'
import jwt from 'jsonwebtoken'
import { host, login } from './jest/helpers'
const factory = Factory()
beforeEach(async () => {
await factory.create('User', {
email: 'test@example.org',
password: '1234'
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('isLoggedIn', () => {
describe('unauthenticated', () => {
it('returns false', async () => {
const query = '{ isLoggedIn }'
await expect(request(host, query)).resolves.toEqual({ isLoggedIn: false })
})
})
})
describe('login', () => {
const mutation = (params) => {
const { email, password } = params
return `
mutation {
login(email:"${email}", password:"${password}"){
token
}
}`
}
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.')
})
})
})
})
describe('CreatePost', () => {
describe('unauthenticated', () => {
let client
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(`mutation {
CreatePost(
title: "I am a post",
content: "Some content"
) { slug }
}`)).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 {
CreatePost(
title: "A title",
content: "Some content"
) { title, content }
}`, { headers })
})
it('creates a post', () => {
expect(response).toEqual({ CreatePost: { title: 'A title', content: 'Some content' } })
})
it('assigns the authenticated user as author', async () => {
const { User } = await client.request(`{
User(email:"test@example.org") {
contributions {
title
}
}
}`, { headers })
expect(User).toEqual([ { contributions: [ { title: 'A title' } ] } ])
})
})
})
})
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 }
})
})
})
})
})

View File

@ -7,12 +7,10 @@ export const host = 'http://127.0.0.1:4123'
export async function login ({ email, password }) {
const mutation = `
mutation {
login(email:"${email}", password:"${password}"){
token
}
login(email:"${email}", password:"${password}")
}`
const response = await request(host, mutation)
return {
authorization: `Bearer ${response.login.token}`
authorization: `Bearer ${response.login}`
}
}

View File

@ -1,17 +0,0 @@
import jwt from 'jsonwebtoken'
import ms from 'ms'
// Generate an Access Token for the given User ID
export default function generateJwt (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

@ -1,42 +0,0 @@
import { Strategy } from 'passport-jwt'
import { fixUrl } from '../middleware/fixImageUrlsMiddleware'
const cookieExtractor = (req) => {
var token = null
if (req && req.cookies) {
token = req.cookies['jwt']
}
return token
}
export default (driver) => {
const options = {
jwtFromRequest: cookieExtractor,
secretOrKey: process.env.JWT_SECRET,
issuer: process.env.GRAPHQL_URI,
audience: process.env.CLIENT_URI
}
return new Strategy(options,
async (JWTPayload, next) => {
const session = driver.session()
const result = await session.run(
'MATCH (user:User {id: $userId}) ' +
'RETURN user {.id, .slug, .name, .avatar, .email, .role} as user LIMIT 1',
{
userId: JWTPayload.id
}
)
session.close()
const [currentUser] = await result.records.map((record) => {
return record.get('user')
})
if (currentUser) {
currentUser.avatar = fixUrl(currentUser.avatar)
return next(null, currentUser)
} else {
return next(null, false)
}
})
}

View File

@ -2,29 +2,21 @@ export default {
Mutation: {
CreateUser: async (resolve, root, args, context, info) => {
args.createdAt = (new Date()).toISOString()
args.disabled = false
args.deleted = false
const result = await resolve(root, args, context, info)
return result
},
CreatePost: async (resolve, root, args, context, info) => {
args.createdAt = (new Date()).toISOString()
args.disabled = false
args.deleted = false
const result = await resolve(root, args, context, info)
return result
},
CreateComment: async (resolve, root, args, context, info) => {
args.createdAt = (new Date()).toISOString()
args.disabled = false
args.deleted = false
const result = await resolve(root, args, context, info)
return result
},
CreateOrganization: async (resolve, root, args, context, info) => {
args.createdAt = (new Date()).toISOString()
args.disabled = false
args.deleted = false
const result = await resolve(root, args, context, info)
return result
},

View File

@ -1,4 +1,4 @@
import { rule, shield, allow } from 'graphql-shield'
import { rule, shield, allow, or } from 'graphql-shield'
/*
* TODO: implement
@ -7,31 +7,58 @@ import { rule, shield, allow } from 'graphql-shield'
const isAuthenticated = rule()(async (parent, args, ctx, info) => {
return ctx.user !== null
})
/*
const isAdmin = rule()(async (parent, args, ctx, info) => {
return ctx.user.role === 'ADMIN'
})
const isModerator = rule()(async (parent, args, ctx, info) => {
return ctx.user.role === 'MODERATOR'
})
*/
const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, ctx, info) => {
return ctx.user.id === parent.id
const isModerator = rule()(async (parent, args, { user }, info) => {
return user && (user.role === 'moderator' || user.role === 'admin')
})
const isAdmin = rule()(async (parent, args, { user }, info) => {
return user && (user.role === 'admin')
})
const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, context, info) => {
return context.user.id === parent.id
})
const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, info) => {
const { disabled, deleted } = args
return !(disabled || deleted)
})
const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => {
if (!user) return false
const session = driver.session()
const { id: postId } = args
const result = await session.run(`
MATCH (post:Post {id: $postId})<-[:WROTE]-(author)
RETURN author
`, { postId })
const [author] = result.records.map((record) => {
return record.get('author')
})
const { properties: { id: authorId } } = author
session.close()
return authorId === user.id
})
// Permissions
const permissions = shield({
Query: {
statistics: allow
// fruits: and(isAuthenticated, or(isAdmin, isModerator)),
// customers: and(isAuthenticated, isAdmin)
statistics: allow,
currentUser: allow,
Post: or(onlyEnabledContent, isModerator)
},
Mutation: {
CreatePost: isAuthenticated,
// TODO UpdatePost: isOwner,
// TODO DeletePost: isOwner,
report: isAuthenticated
UpdatePost: isAuthor,
DeletePost: isAuthor,
report: isAuthenticated,
CreateBadge: isAdmin,
UpdateBadge: isAdmin,
DeleteBadge: isAdmin,
enable: isModerator,
disable: isModerator
// addFruitToBasket: isAuthenticated
// CreateUser: allow,
},
@ -39,7 +66,6 @@ const permissions = shield({
email: isMyOwn,
password: isMyOwn
}
// Post: isAuthenticated
})
export default permissions

View File

@ -1,38 +1,26 @@
const setDefaults = (args) => {
if (typeof args.deleted !== 'boolean') {
args.deleted = false
}
if (typeof args.disabled !== 'boolean') {
args.disabled = false
}
return args
}
export default {
Query: {
Post: async (resolve, root, args, context, info) => {
if (typeof args.deleted !== 'boolean') {
args.deleted = false
}
if (typeof args.disabled !== 'boolean') {
args.disabled = false
}
const result = await resolve(root, args, context, info)
return result
Post: (resolve, root, args, context, info) => {
return resolve(root, setDefaults(args), context, info)
},
Comment: async (resolve, root, args, context, info) => {
if (typeof args.deleted !== 'boolean') {
args.deleted = false
}
if (typeof args.disabled !== 'boolean') {
args.disabled = false
}
const result = await resolve(root, args, context, info)
return result
return resolve(root, setDefaults(args), context, info)
},
User: async (resolve, root, args, context, info) => {
if (typeof args.deleted !== 'boolean') {
args.deleted = false
}
if (typeof args.disabled !== 'boolean') {
args.disabled = false
}
// console.log('ROOT', root)
// console.log('ARGS', args)
// console.log('CONTEXT', context)
// console.log('info', info.fieldNodes[0].arguments)
const result = await resolve(root, args, context, info)
return result
return resolve(root, setDefaults(args), context, info)
}
},
Mutation: async (resolve, root, args, context, info) => {
return resolve(root, setDefaults(args), context, info)
}
}

View File

@ -1,22 +1,46 @@
type Query {
isLoggedIn: Boolean!
"Get the currently logged in User based on the given JWT Token"
currentUser: User
"Get the latest Network Statistics"
statistics: Statistics!
}
type Mutation {
login(email: String!, password: String!): LoggedInUser
"Get a JWT Token for the given Email and password"
login(email: String!, password: String!): String!
signup(email: String!, password: String!): Boolean!
report(resource: Resource!, description: String): Report
}
type LoggedInUser {
id: ID!
slug: String!
name: String!
avatar:String!
email: String!
role: String!
locationName: String
about: String
token: String!
"Shout the given Type and ID"
shout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """
MATCH (n {id: $id})<-[:WROTE]-(wu:User), (u:User {id: $cypherParams.currentUserId})
WHERE $type IN labels(n) AND NOT wu.id = $cypherParams.currentUserId
MERGE (u)-[r:SHOUTED]->(n)
RETURN COUNT(r) > 0
""")
"Unshout the given Type and ID"
unshout(id: ID!, type: ShoutTypeEnum): Boolean! @cypher(statement: """
MATCH (:User {id: $cypherParams.currentUserId})-[r:SHOUTED]->(n {id: $id})
WHERE $type IN labels(n)
DELETE r
RETURN COUNT(r) > 0
""")
"Follow the given Type and ID"
follow(id: ID!, type: FollowTypeEnum): Boolean! @cypher(statement: """
MATCH (n {id: $id}), (u:User {id: $cypherParams.currentUserId})
WHERE $type IN labels(n) AND NOT $id = $cypherParams.currentUserId
MERGE (u)-[r:FOLLOWS]->(n)
RETURN COUNT(r) > 0
""")
"Unfollow the given Type and ID"
unfollow(id: ID!, type: FollowTypeEnum): Boolean! @cypher(statement: """
MATCH (:User {id: $cypherParams.currentUserId})-[r:FOLLOWS]->(n {id: $id})
WHERE $type IN labels(n)
DELETE r
RETURN COUNT(r) > 0
""")
disable(resource: Resource!): Boolean!
enable(resource: Resource!): Boolean!
}
type Statistics {
@ -85,6 +109,7 @@ type User {
avatar: String
deleted: Boolean
disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN")
role: UserGroupEnum
publicKey: String
privateKey: String
@ -97,13 +122,19 @@ type User {
updatedAt: String
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(r)")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(r)")
followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)")
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(r)")
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
"Is the currently logged in user following that user?"
followedByCurrentUser: Boolean! @cypher(statement: """
MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
""")
#contributions: [WrittenPost]!
#contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
@ -122,7 +153,7 @@ type User {
commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true RETURN COUNT(r)")
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
@ -147,6 +178,7 @@ type Post {
visibility: VisibilityEnum
deleted: Boolean
disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN")
createdAt: String
updatedAt: String
@ -163,7 +195,13 @@ type Post {
commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) RETURN COUNT(r)")
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
"Has the currently logged in user shouted that post?"
shoutedByCurrentUser: Boolean! @cypher(statement: """
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
""")
}
type Comment {
@ -177,6 +215,7 @@ type Comment {
updatedAt: String
deleted: Boolean
disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN")
}
type Report {
@ -217,6 +256,16 @@ enum BadgeStatusEnum {
permanent
temporary
}
enum ShoutTypeEnum {
Post
Organization
Project
}
enum FollowTypeEnum {
User
Organization
Project
}
type Organization {
id: ID!
@ -238,7 +287,7 @@ type Tag {
name: String!
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN")
taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(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)")
deleted: Boolean
disabled: Boolean

View File

@ -10,14 +10,14 @@ export default function (params) {
} = params
return `
mutation {
CreateBadge(
id: "${id}",
key: "${key}",
type: ${type},
status: ${status},
icon: "${icon}"
) { id }
}
mutation {
CreateBadge(
id: "${id}",
key: "${key}",
type: ${type},
status: ${status},
icon: "${icon}"
) { id }
}
`
}

View File

@ -15,13 +15,11 @@ export const seedServerHost = 'http://127.0.0.1:4001'
const authenticatedHeaders = async ({ email, password }, host) => {
const mutation = `
mutation {
login(email:"${email}", password:"${password}"){
token
}
login(email:"${email}", password:"${password}")
}`
const response = await request(host, mutation)
return {
authorization: `Bearer ${response.login.token}`
authorization: `Bearer ${response.login}`
}
}
const factories = {
@ -88,6 +86,36 @@ export default function Factory (options = {}) {
this.lastResponse = await this.graphQLClient.request(mutation)
return this
},
async mutate (mutation, variables) {
this.lastResponse = await this.graphQLClient.request(mutation, variables)
return this
},
async shout (properties) {
const { id, type } = properties
const mutation = `
mutation {
shout(
id: "${id}",
type: ${type}
)
}
`
this.lastResponse = await this.graphQLClient.request(mutation)
return this
},
async follow (properties) {
const { id, type } = properties
const mutation = `
mutation {
follow(
id: "${id}",
type: ${type}
)
}
`
this.lastResponse = await this.graphQLClient.request(mutation)
return this
},
async cleanDatabase () {
this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver })
return this
@ -96,6 +124,7 @@ export default function Factory (options = {}) {
result.authenticateAs.bind(result)
result.create.bind(result)
result.relate.bind(result)
result.mutate.bind(result)
result.cleanDatabase.bind(result)
return result
}

View File

@ -4,7 +4,7 @@ import uuid from 'uuid/v4'
export default function create (params) {
const {
id = uuid(),
name = faker.comany.companyName(),
name = faker.company.companyName(),
description = faker.company.catchPhrase(),
disabled = false,
deleted = false

View File

@ -14,7 +14,6 @@ export default function (params) {
].join('. '),
image = faker.image.image(),
visibility = 'public',
disabled = false,
deleted = false
} = params
@ -26,7 +25,6 @@ export default function (params) {
content: "${content}",
image: "${image}",
visibility: ${visibility},
disabled: ${disabled},
deleted: ${deleted}
) { title, content }
}

View File

@ -25,10 +25,13 @@ export default function create (params) {
disabled: ${disabled},
deleted: ${deleted}
) {
id
name
email
avatar
role
deleted
disabled
}
}
`

View File

@ -23,6 +23,15 @@ import Factory from './factories'
f.create('User', { id: 'u7', name: 'Dagobert', role: 'user', email: 'dagobert@example.org' })
])
const [ asAdmin, asModerator, asUser, asTick, asTrick, asTrack ] = await Promise.all([
Factory().authenticateAs({ email: 'admin@example.org', password: '1234' }),
Factory().authenticateAs({ email: 'moderator@example.org', password: '1234' }),
Factory().authenticateAs({ email: 'user@example.org', password: '1234' }),
Factory().authenticateAs({ email: 'tick@example.org', password: '1234' }),
Factory().authenticateAs({ email: 'trick@example.org', password: '1234' }),
Factory().authenticateAs({ email: 'track@example.org', password: '1234' })
])
await Promise.all([
f.relate('User', 'Badges', { from: 'b6', to: 'u1' }),
f.relate('User', 'Badges', { from: 'b5', to: 'u2' }),
@ -30,12 +39,6 @@ import Factory from './factories'
f.relate('User', 'Badges', { from: 'b3', to: 'u4' }),
f.relate('User', 'Badges', { from: 'b2', to: 'u5' }),
f.relate('User', 'Badges', { from: 'b1', to: 'u6' }),
f.relate('User', 'Following', { from: 'u1', to: 'u2' }),
f.relate('User', 'Following', { from: 'u2', to: 'u3' }),
f.relate('User', 'Following', { from: 'u3', to: 'u4' }),
f.relate('User', 'Following', { from: 'u4', to: 'u5' }),
f.relate('User', 'Following', { from: 'u5', to: 'u6' }),
f.relate('User', 'Following', { from: 'u6', to: 'u7' }),
f.relate('User', 'Friends', { from: 'u1', to: 'u2' }),
f.relate('User', 'Friends', { from: 'u1', to: 'u3' }),
f.relate('User', 'Friends', { from: 'u2', to: 'u3' }),
@ -44,6 +47,21 @@ import Factory from './factories'
f.relate('User', 'Blacklisted', { from: 'u7', to: 'u6' })
])
await Promise.all([
asAdmin
.follow({ id: 'u3', type: 'User' }),
asModerator
.follow({ id: 'u4', type: 'User' }),
asUser
.follow({ id: 'u4', type: 'User' }),
asTick
.follow({ id: 'u6', type: 'User' }),
asTrick
.follow({ id: 'u4', type: 'User' }),
asTrack
.follow({ id: 'u3', type: 'User' })
])
await Promise.all([
f.create('Category', { id: 'cat1', name: 'Just For Fun', slug: 'justforfun', icon: 'smile' }),
f.create('Category', { id: 'cat2', name: 'Happyness & Values', slug: 'happyness-values', icon: 'heart-o' }),
@ -70,19 +88,10 @@ import Factory from './factories'
f.create('Tag', { id: 't4', name: 'Freiheit' })
])
const [ asAdmin, asModerator, asUser, asTick, asTrick, asTrack ] = await Promise.all([
Factory().authenticateAs({ email: 'admin@example.org', password: '1234' }),
Factory().authenticateAs({ email: 'moderator@example.org', password: '1234' }),
Factory().authenticateAs({ email: 'user@example.org', password: '1234' }),
Factory().authenticateAs({ email: 'tick@example.org', password: '1234' }),
Factory().authenticateAs({ email: 'trick@example.org', password: '1234' }),
Factory().authenticateAs({ email: 'track@example.org', password: '1234' })
])
await Promise.all([
asAdmin.create('Post', { id: 'p0' }),
asModerator.create('Post', { id: 'p1' }),
asUser.create('Post', { id: 'p2' }),
asUser.create('Post', { id: 'p2', deleted: true }),
asTick.create('Post', { id: 'p3' }),
asTrick.create('Post', { id: 'p4' }),
asTrack.create('Post', { id: 'p5' }),
@ -98,6 +107,16 @@ import Factory from './factories'
asTick.create('Post', { id: 'p15' })
])
const disableMutation = `
mutation {
disable(resource: {
id: "p11"
type: contribution
})
}
`
await asModerator.mutate(disableMutation)
await Promise.all([
f.relate('Post', 'Categories', { from: 'p0', to: 'cat16' }),
f.relate('Post', 'Categories', { from: 'p1', to: 'cat1' }),
@ -133,13 +152,26 @@ import Factory from './factories'
f.relate('Post', 'Tags', { from: 'p14', to: 't2' }),
f.relate('Post', 'Tags', { from: 'p15', to: 't3' })
])
await Promise.all([
f.relate('User', 'Shouted', { from: 'u1', to: 'p2' }),
f.relate('User', 'Shouted', { from: 'u1', to: 'p3' }),
f.relate('User', 'Shouted', { from: 'u2', to: 'p1' }),
f.relate('User', 'Shouted', { from: 'u3', to: 'p1' }),
f.relate('User', 'Shouted', { from: 'u3', to: 'p4' }),
f.relate('User', 'Shouted', { from: 'u4', to: 'p1' })
asAdmin
.shout({ id: 'p2', type: 'Post' }),
asAdmin
.shout({ id: 'p6', type: 'Post' }),
asModerator
.shout({ id: 'p0', type: 'Post' }),
asModerator
.shout({ id: 'p6', type: 'Post' }),
asUser
.shout({ id: 'p6', type: 'Post' }),
asUser
.shout({ id: 'p7', type: 'Post' }),
asTick
.shout({ id: 'p8', type: 'Post' }),
asTick
.shout({ id: 'p9', type: 'Post' }),
asTrack
.shout({ id: 'p10', type: 'Post' })
])
await Promise.all([

View File

@ -8,11 +8,8 @@ import middleware from './middleware'
import applyDirectives from './bootstrap/directives'
import applyScalars from './bootstrap/scalars'
import { getDriver } from './bootstrap/neo4j'
import passport from 'passport'
import jwtStrategy from './jwt/strategy'
import jwt from 'jsonwebtoken'
import helmet from 'helmet'
import decode from './jwt/decode'
dotenv.config()
// check env and warn
@ -43,20 +40,17 @@ schema = applyScalars(applyDirectives(schema))
const createServer = (options) => {
const defaults = {
context: async (req) => {
const payload = {
context: async ({ request }) => {
const authorizationHeader = request.headers.authorization || ''
const user = await decode(driver, authorizationHeader)
return {
driver,
user: null,
req: req.request
user,
req: request,
cypherParams: {
currentUserId: user ? user.id : null
}
}
try {
const token = payload.req.headers.authorization.replace('Bearer ', '')
payload.user = await jwt.verify(token, process.env.JWT_SECRET)
} catch (err) {
// nothing
}
return payload
},
schema: schema,
debug: debug,
@ -66,12 +60,8 @@ const createServer = (options) => {
}
const server = new GraphQLServer(Object.assign({}, defaults, options))
passport.use('jwt', jwtStrategy(driver))
server.express.use(helmet())
server.express.use(passport.initialize())
server.express.use(express.static('public'))
server.express.post('/graphql', passport.authenticate(['jwt'], { session: false }))
return server
}

1463
yarn.lock

File diff suppressed because it is too large Load Diff