mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #187 from Human-Connection/194_stop_decoding_jwt_on_frontend
Implement currentUser query
This commit is contained in:
commit
086345cd35
@ -59,8 +59,6 @@
|
|||||||
"neo4j-graphql-js": "~2.3.1",
|
"neo4j-graphql-js": "~2.3.1",
|
||||||
"node-fetch": "~2.3.0",
|
"node-fetch": "~2.3.0",
|
||||||
"npm-run-all": "~4.1.5",
|
"npm-run-all": "~4.1.5",
|
||||||
"passport": "~0.4.0",
|
|
||||||
"passport-jwt": "~4.0.0",
|
|
||||||
"sanitize-html": "~1.20.0",
|
"sanitize-html": "~1.20.0",
|
||||||
"slug": "~1.0.0",
|
"slug": "~1.0.0",
|
||||||
"trunc-html": "~1.1.2",
|
"trunc-html": "~1.1.2",
|
||||||
|
|||||||
@ -1,182 +1,22 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import bcrypt from 'bcryptjs'
|
import userManagement from './resolvers/user_management.js'
|
||||||
import generateJwt from './jwt/generateToken'
|
import statistics from './resolvers/statistics.js'
|
||||||
import uuid from 'uuid/v4'
|
import reports from './resolvers/reports.js'
|
||||||
import { fixUrl } from './middleware/fixImageUrlsMiddleware'
|
import posts from './resolvers/posts.js'
|
||||||
import { AuthenticationError } from 'apollo-server'
|
|
||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
|
||||||
|
|
||||||
export const typeDefs =
|
export const typeDefs =
|
||||||
fs.readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql'))
|
fs.readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql'))
|
||||||
.toString('utf-8')
|
.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 = {
|
export const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
isLoggedIn: (parent, args, { driver, user }) => {
|
...statistics.Query,
|
||||||
return Boolean(user && user.id)
|
...userManagement.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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// usersBySubstring: neo4jgraphql
|
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
signup: async (parent, { email, password }, { req }) => {
|
...userManagement.Mutation,
|
||||||
// if (data[email]) {
|
...reports.Mutation,
|
||||||
// throw new Error('Another User with same email exists.')
|
...posts.Mutation
|
||||||
// }
|
|
||||||
// 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) => {
|
|
||||||
const result = await neo4jgraphql(object, params, ctx, resolveInfo, false)
|
|
||||||
|
|
||||||
const session = ctx.driver.session()
|
|
||||||
await session.run(
|
|
||||||
'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' +
|
|
||||||
'MERGE (post)<-[:WROTE]-(author) ' +
|
|
||||||
'RETURN author', {
|
|
||||||
userId: ctx.user.id,
|
|
||||||
postId: result.id
|
|
||||||
})
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -7,12 +7,10 @@ export const host = 'http://127.0.0.1:4123'
|
|||||||
export async function login ({ email, password }) {
|
export async function login ({ email, password }) {
|
||||||
const mutation = `
|
const mutation = `
|
||||||
mutation {
|
mutation {
|
||||||
login(email:"${email}", password:"${password}"){
|
login(email:"${email}", password:"${password}")
|
||||||
token
|
|
||||||
}
|
|
||||||
}`
|
}`
|
||||||
const response = await request(host, mutation)
|
const response = await request(host, mutation)
|
||||||
return {
|
return {
|
||||||
authorization: `Bearer ${response.login.token}`
|
authorization: `Bearer ${response.login}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/jwt/decode.js
Normal file
29
src/jwt/decode.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken'
|
|||||||
import ms from 'ms'
|
import ms from 'ms'
|
||||||
|
|
||||||
// Generate an Access Token for the given User ID
|
// Generate an Access Token for the given User ID
|
||||||
export default function generateJwt (user) {
|
export default function encode (user) {
|
||||||
const token = jwt.sign(user, process.env.JWT_SECRET, {
|
const token = jwt.sign(user, process.env.JWT_SECRET, {
|
||||||
expiresIn: ms('1d'),
|
expiresIn: ms('1d'),
|
||||||
issuer: process.env.GRAPHQL_URI,
|
issuer: process.env.GRAPHQL_URI,
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -23,7 +23,8 @@ const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, ctx, info) => {
|
|||||||
// Permissions
|
// Permissions
|
||||||
const permissions = shield({
|
const permissions = shield({
|
||||||
Query: {
|
Query: {
|
||||||
statistics: allow
|
statistics: allow,
|
||||||
|
currentUser: allow
|
||||||
// fruits: and(isAuthenticated, or(isAdmin, isModerator)),
|
// fruits: and(isAuthenticated, or(isAdmin, isModerator)),
|
||||||
// customers: and(isAuthenticated, isAdmin)
|
// customers: and(isAuthenticated, isAdmin)
|
||||||
},
|
},
|
||||||
|
|||||||
22
src/resolvers/posts.js
Normal file
22
src/resolvers/posts.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Mutation: {
|
||||||
|
CreatePost: async (object, params, ctx, resolveInfo) => {
|
||||||
|
const result = await neo4jgraphql(object, params, ctx, resolveInfo, false)
|
||||||
|
|
||||||
|
const session = ctx.driver.session()
|
||||||
|
await session.run(
|
||||||
|
'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' +
|
||||||
|
'MERGE (post)<-[:WROTE]-(author) ' +
|
||||||
|
'RETURN author', {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
postId: result.id
|
||||||
|
})
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/resolvers/posts.spec.js
Normal file
61
src/resolvers/posts.spec.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import Factory from '../seed/factories'
|
||||||
|
import { GraphQLClient } from 'graphql-request'
|
||||||
|
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('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' } ] } ])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
51
src/resolvers/reports.js
Normal file
51
src/resolvers/reports.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/resolvers/reports.spec.js
Normal file
68
src/resolvers/reports.spec.js
Normal 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 }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
67
src/resolvers/statistics.js
Normal file
67
src/resolvers/statistics.js
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/resolvers/user_management.js
Normal file
51
src/resolvers/user_management.js
Normal 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.')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
179
src/resolvers/user_management.spec.js
Normal file
179
src/resolvers/user_management.spec.js
Normal 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.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,23 +1,13 @@
|
|||||||
type Query {
|
type Query {
|
||||||
isLoggedIn: Boolean!
|
isLoggedIn: Boolean!
|
||||||
|
currentUser: User
|
||||||
statistics: Statistics!
|
statistics: Statistics!
|
||||||
}
|
}
|
||||||
type Mutation {
|
type Mutation {
|
||||||
login(email: String!, password: String!): LoggedInUser
|
login(email: String!, password: String!): String!
|
||||||
signup(email: String!, password: String!): Boolean!
|
signup(email: String!, password: String!): Boolean!
|
||||||
report(resource: Resource!, description: String): Report
|
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!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Statistics {
|
type Statistics {
|
||||||
countUsers: Int!
|
countUsers: Int!
|
||||||
|
|||||||
@ -15,13 +15,11 @@ export const seedServerHost = 'http://127.0.0.1:4001'
|
|||||||
const authenticatedHeaders = async ({ email, password }, host) => {
|
const authenticatedHeaders = async ({ email, password }, host) => {
|
||||||
const mutation = `
|
const mutation = `
|
||||||
mutation {
|
mutation {
|
||||||
login(email:"${email}", password:"${password}"){
|
login(email:"${email}", password:"${password}")
|
||||||
token
|
|
||||||
}
|
|
||||||
}`
|
}`
|
||||||
const response = await request(host, mutation)
|
const response = await request(host, mutation)
|
||||||
return {
|
return {
|
||||||
authorization: `Bearer ${response.login.token}`
|
authorization: `Bearer ${response.login}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const factories = {
|
const factories = {
|
||||||
|
|||||||
@ -8,10 +8,7 @@ import middleware from './middleware'
|
|||||||
import applyDirectives from './bootstrap/directives'
|
import applyDirectives from './bootstrap/directives'
|
||||||
import applyScalars from './bootstrap/scalars'
|
import applyScalars from './bootstrap/scalars'
|
||||||
import { getDriver } from './bootstrap/neo4j'
|
import { getDriver } from './bootstrap/neo4j'
|
||||||
|
import decode from './jwt/decode'
|
||||||
import passport from 'passport'
|
|
||||||
import jwtStrategy from './jwt/strategy'
|
|
||||||
import jwt from 'jsonwebtoken'
|
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
// check env and warn
|
// check env and warn
|
||||||
@ -42,20 +39,14 @@ schema = applyScalars(applyDirectives(schema))
|
|||||||
|
|
||||||
const createServer = (options) => {
|
const createServer = (options) => {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
context: async (req) => {
|
context: async ({ request }) => {
|
||||||
const payload = {
|
const authorizationHeader = request.headers.authorization || ''
|
||||||
|
const user = await decode(driver, authorizationHeader)
|
||||||
|
return {
|
||||||
driver,
|
driver,
|
||||||
user: null,
|
user,
|
||||||
req: req.request
|
req: request
|
||||||
}
|
}
|
||||||
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,
|
schema: schema,
|
||||||
debug: debug,
|
debug: debug,
|
||||||
@ -65,11 +56,7 @@ const createServer = (options) => {
|
|||||||
}
|
}
|
||||||
const server = new GraphQLServer(Object.assign({}, defaults, options))
|
const server = new GraphQLServer(Object.assign({}, defaults, options))
|
||||||
|
|
||||||
passport.use('jwt', jwtStrategy(driver))
|
|
||||||
server.express.use(passport.initialize())
|
|
||||||
server.express.use(express.static('public'))
|
server.express.use(express.static('public'))
|
||||||
|
|
||||||
server.express.post('/graphql', passport.authenticate(['jwt'], { session: false }))
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
yarn.lock
28
yarn.lock
@ -4412,7 +4412,7 @@ jsonify@~0.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
||||||
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
|
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
|
||||||
|
|
||||||
jsonwebtoken@^8.2.0, jsonwebtoken@~8.5.0:
|
jsonwebtoken@~8.5.0:
|
||||||
version "8.5.0"
|
version "8.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#ebd0ca2a69797816e1c5af65b6c759787252947e"
|
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#ebd0ca2a69797816e1c5af65b6c759787252947e"
|
||||||
integrity sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA==
|
integrity sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA==
|
||||||
@ -5412,27 +5412,6 @@ pascalcase@^0.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
||||||
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
|
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
|
||||||
|
|
||||||
passport-jwt@~4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.0.tgz#7f0be7ba942e28b9f5d22c2ebbb8ce96ef7cf065"
|
|
||||||
integrity sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==
|
|
||||||
dependencies:
|
|
||||||
jsonwebtoken "^8.2.0"
|
|
||||||
passport-strategy "^1.0.0"
|
|
||||||
|
|
||||||
passport-strategy@1.x.x, passport-strategy@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
|
|
||||||
integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=
|
|
||||||
|
|
||||||
passport@~0.4.0:
|
|
||||||
version "0.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.0.tgz#c5095691347bd5ad3b5e180238c3914d16f05811"
|
|
||||||
integrity sha1-xQlWkTR71a07XhgCOMORTRbwWBE=
|
|
||||||
dependencies:
|
|
||||||
passport-strategy "1.x.x"
|
|
||||||
pause "0.0.1"
|
|
||||||
|
|
||||||
path-dirname@^1.0.0:
|
path-dirname@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
|
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
|
||||||
@ -5487,11 +5466,6 @@ pathval@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
|
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
|
||||||
integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA=
|
integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA=
|
||||||
|
|
||||||
pause@0.0.1:
|
|
||||||
version "0.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
|
|
||||||
integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=
|
|
||||||
|
|
||||||
performance-now@^2.1.0:
|
performance-now@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user