From 286cfa992be34dd7b4deb376ea1f3a14c6ecdcfe Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sat, 13 Jul 2019 15:50:59 +0200 Subject: [PATCH] Prepare to implement scanner --- api/package.json | 1 + api/src/Scanner.js | 77 ++++++++++++++++++++++++++++++++++++ api/src/graphql-schema.js | 13 ------ api/src/index.js | 65 ++++++++++++++++++++++++++---- api/src/resolvers/scanner.js | 34 ++++++++++++++++ api/src/resolvers/users.js | 14 ++++--- api/src/schema.graphql | 15 ++++++- api/src/token.js | 41 +++++++++++++++++++ 8 files changed, 233 insertions(+), 27 deletions(-) create mode 100644 api/src/Scanner.js delete mode 100644 api/src/graphql-schema.js create mode 100644 api/src/resolvers/scanner.js create mode 100644 api/src/token.js diff --git a/api/package.json b/api/package.json index 373ad568..3d7bd571 100644 --- a/api/package.json +++ b/api/package.json @@ -14,6 +14,7 @@ "author": "William Lyon", "license": "MIT", "dependencies": { + "@babel/node": "^7.5.0", "@babel/plugin-transform-spread": "^7.2.2", "apollo-boost": "^0.4.3", "apollo-cache-inmemory": "^1.5.1", diff --git a/api/src/Scanner.js b/api/src/Scanner.js new file mode 100644 index 00000000..d4f06f66 --- /dev/null +++ b/api/src/Scanner.js @@ -0,0 +1,77 @@ +import { PubSub } from 'apollo-server' + +export const EVENT_SCANNER_PROGRESS = 'SCANNER_PROGRESS' + +class PhotoScanner { + constructor(driver) { + this.driver = driver + this.isRunning = false + this.pubsub = new PubSub() + } + + async scanAll() { + this.isRunning = true + + this.pubsub.publish(EVENT_SCANNER_PROGRESS, { + scannerStatusUpdate: { + progress: 0, + finished: false, + error: false, + errorMessage: '', + }, + }) + + let session = this.driver.session() + + session + .run( + 'MATCH (u:User) return u.id AS id, u.username AS username, u.rootPath as path' + ) + .subscribe({ + onNext: function(record) { + const username = record.get('username') + const id = record.get('id') + const path = record.get('path') + + if (!path) { + console.log(`User ${username}, has no root path, skipping`) + return + } + + this.scanUser(id) + + console.log(`Scanning ${username}...`) + }, + onCompleted: () => { + session.close() + this.isRunning = false + console.log('Done scanning') + + this.pubsub.publish(EVENT_SCANNER_PROGRESS, { + scannerStatusUpdate: { + progress: 100, + finished: true, + error: false, + errorMessage: '', + }, + }) + }, + onError: error => { + console.error(error) + + this.pubsub.publish(EVENT_SCANNER_PROGRESS, { + scannerStatusUpdate: { + progress: 0, + finished: false, + error: true, + errorMessage: error.message, + }, + }) + }, + }) + } + + async scanUser(id) {} +} + +export default PhotoScanner diff --git a/api/src/graphql-schema.js b/api/src/graphql-schema.js deleted file mode 100644 index 0b6592fe..00000000 --- a/api/src/graphql-schema.js +++ /dev/null @@ -1,13 +0,0 @@ -import fs from "fs"; -import path from "path"; - -/* - * Check for GRAPHQL_SCHEMA environment variable to specify schema file - * fallback to schema.graphql if GRAPHQL_SCHEMA environment variable is not set - */ - -export const typeDefs = fs - .readFileSync( - process.env.GRAPHQL_SCHEMA || path.join(__dirname, "schema.graphql") - ) - .toString("utf-8"); diff --git a/api/src/index.js b/api/src/index.js index fa44f963..67b92b0a 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -1,10 +1,15 @@ -import { typeDefs } from './graphql-schema' +import fs from 'fs' +import path from 'path' import { ApolloServer } from 'apollo-server-express' import express from 'express' import bodyParser from 'body-parser' import { v1 as neo4j } from 'neo4j-driver' import { makeAugmentedSchema } from 'neo4j-graphql-js' import dotenv from 'dotenv' +import http from 'http' +import PhotoScanner from './Scanner' + +import { getUserFromToken, getTokenFromBearer } from './token' // set environment variables from ../.env dotenv.config() @@ -20,7 +25,14 @@ app.use(bodyParser.json()) * https://grandstack.io/docs/neo4j-graphql-js-api.html#makeaugmentedschemaoptions-graphqlschema */ -import users from './resolvers/users' +const typeDefs = fs + .readFileSync( + process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql') + ) + .toString('utf-8') + +import usersResolver from './resolvers/users' +import scannerResolver from './resolvers/scanner' const schema = makeAugmentedSchema({ typeDefs, @@ -30,10 +42,17 @@ const schema = makeAugmentedSchema({ hasRole: true, }, mutation: false, + query: { + exclude: ['ScannerResult', 'AuthorizeResult', 'Subscription'], + }, }, resolvers: { Mutation: { - ...users.mutation, + ...usersResolver.mutation, + ...scannerResolver.mutation, + }, + Subscription: { + ...scannerResolver.subscription, }, }, }) @@ -51,6 +70,8 @@ const driver = neo4j.driver( ) ) +const scanner = new PhotoScanner(driver) + /* * Create a new ApolloServer instance, serving the GraphQL schema * created using makeAugmentedSchema above and injecting the Neo4j driver @@ -58,22 +79,50 @@ const driver = neo4j.driver( * generated resolvers to connect to the database. */ const server = new ApolloServer({ - context: ({ req }) => Object.assign(req, { driver }), + context: async function({ req }) { + let user = null + + if (req && req.headers.authorization) { + const token = getTokenFromBearer(req.headers.authorization) + user = await getUserFromToken(token, driver) + } + + return { ...req, driver, scanner, user } + }, schema: schema, introspection: true, playground: true, + subscriptions: { + onConnect: async (connectionParams, webSocket) => { + const token = getTokenFromBearer(connectionParams.Authorization) + const user = await getUserFromToken(token, driver) + + return { + token, + user, + } + }, + }, }) // Specify port and path for GraphQL endpoint const port = process.env.GRAPHQL_LISTEN_PORT || 4001 -const path = '/graphql' +const graphPath = '/graphql' /* * Optionally, apply Express middleware for authentication, etc * This also also allows us to specify a path for the GraphQL endpoint */ -server.applyMiddleware({ app, path }) +server.applyMiddleware({ app, graphPath }) -app.listen({ port, path }, () => { - console.log(`GraphQL server ready at http://localhost:${port}${path}`) +const httpServer = http.createServer(app) +server.installSubscriptionHandlers(httpServer) + +httpServer.listen({ port, graphPath }, () => { + console.log( + `🚀 GraphQL endpoint ready at http://localhost:${port}${server.graphqlPath}` + ) + console.log( + `🚀 Subscriptions ready at ws://localhost:${port}${server.subscriptionsPath}` + ) }) diff --git a/api/src/resolvers/scanner.js b/api/src/resolvers/scanner.js new file mode 100644 index 00000000..55a9a3a2 --- /dev/null +++ b/api/src/resolvers/scanner.js @@ -0,0 +1,34 @@ +import { EVENT_SCANNER_PROGRESS } from '../scanner' + +const mutation = { + async scanAll(root, args, ctx, info) { + if (ctx.scanner.isRunning) { + return { + finished: false, + error: true, + errorMessage: 'Scanner already running', + } + } + + ctx.scanner.scanAll() + + return { + finished: false, + error: false, + progress: 0, + } + }, +} + +const subscription = { + scannerStatusUpdate: { + subscribe(root, args, ctx, info) { + return ctx.scanner.pubsub.asyncIterator([EVENT_SCANNER_PROGRESS]) + }, + }, +} + +export default { + mutation, + subscription, +} diff --git a/api/src/resolvers/users.js b/api/src/resolvers/users.js index 347efd92..a9c39188 100644 --- a/api/src/resolvers/users.js +++ b/api/src/resolvers/users.js @@ -36,12 +36,12 @@ const mutation = { let { username, password } = args let session = ctx.driver.session() - let result = await session.run( + let findResult = await session.run( 'MATCH (usr:User {username: {username} }) RETURN usr', { username } ) - if (result.records.length > 0) { + if (findResult.records.length > 0) { return { success: false, status: 'Username is already taken', @@ -49,17 +49,21 @@ const mutation = { } } - await session.run( - 'CREATE (n:User { username: {username}, password: {password}, id: {id} }) return n', + const registerResult = await session.run( + 'CREATE (n:User { username: {username}, password: {password}, id: {id} }) return n.id', { username, password, id: uuid() } ) + let id = registerResult.records[0].get('n.id') + + const token = jwt.sign({ id }, process.env.JWT_SECRET) + session.close() return { success: true, status: 'User created', - token: 'yay', + token: token, } }, } diff --git a/api/src/schema.graphql b/api/src/schema.graphql index 488ee410..e0ea072a 100644 --- a/api/src/schema.graphql +++ b/api/src/schema.graphql @@ -15,7 +15,7 @@ type Album @isAuthenticated { id: ID! title: String photos: [Photo] @relation(name: "CONTAINS", direction: "OUT") - owner: User! @ relation(name: "OWNS", direction: "IN") + owner: User! @relation(name: "OWNS", direction: "IN") } type Photo @isAuthenticated { @@ -36,9 +36,22 @@ type AuthorizeResult { token: String } +type ScannerResult { + finished: Boolean! + error: Boolean! + errorMessage: String + progress: Float +} + +type Subscription { + scannerStatusUpdate: ScannerResult +} + type Mutation { authorizeUser(username: String!, password: String!): AuthorizeResult! registerUser(username: String!, password: String!): AuthorizeResult! + + scanAll: ScannerResult! @isAuthenticated } type Query { diff --git a/api/src/token.js b/api/src/token.js new file mode 100644 index 00000000..1f4c6c27 --- /dev/null +++ b/api/src/token.js @@ -0,0 +1,41 @@ +import jwt from 'jsonwebtoken' + +export const getUserFromToken = async function(token, driver) { + const tokenContent = jwt.verify(token, process.env.JWT_SECRET) + const userId = tokenContent.id + + const session = driver.session() + + const userResult = await session.run( + 'MATCH (u:User {id: {userId}}) RETURN u', + { + userId, + } + ) + + if (userResult.records.length == 0) { + throw new Error(`User doesn't exist anymore`) + } + + const user = userResult.records[0].toObject() + + session.close() + + return user +} + +export const getTokenFromBearer = bearer => { + let token = bearer + + if (!token) { + throw new Error('Missing auth token') + } + + if (!token.toLowerCase().startsWith('bearer ')) { + throw new Error('Invalid auth token') + } + + token = token.substr(7) + + return token +}