Delete folder `api-node-old`
Old api can be found on branch `old-nodejs-server`
This commit is contained in:
parent
6e72caf2f0
commit
2bbb1740ba
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": 3
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": ["@babel/plugin-transform-spread"]
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
node_modules/
|
||||
build/
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"require-atomic-updates": "off"
|
||||
},
|
||||
"parser": "babel-eslint"
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
FROM node:10
|
||||
|
||||
ENV PRODUCTION=1
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
|
||||
EXPOSE 4001
|
||||
|
||||
CMD ["npm", "start"]
|
|
@ -1,54 +0,0 @@
|
|||
# GRANDstack Starter - GraphQL API
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Start the GraphQL service:
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
This will start the GraphQL service (by default on localhost:4000) where you can issue GraphQL requests or access GraphQL Playground in the browser:
|
||||
|
||||
![GraphQL Playground](img/graphql-playground.png)
|
||||
|
||||
## Configure
|
||||
|
||||
Set your Neo4j connection string and credentials in `.env`. For example:
|
||||
|
||||
*.env*
|
||||
|
||||
```
|
||||
NEO4J_URI=bolt://localhost:7687
|
||||
NEO4J_USER=neo4j
|
||||
NEO4J_PASSWORD=letmein
|
||||
```
|
||||
|
||||
Note that grand-stack-starter does not currently bundle a distribution of Neo4j. You can download [Neo4j Desktop](https://neo4j.com/download/) and run locally for development, spin up a [hosted Neo4j Sandbox instance](https://neo4j.com/download/), run Neo4j in one of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/), or [spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/). Just be sure to update the Neo4j connection string and credentials accordingly in `.env`.
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy to any service that hosts Node.js apps, but [Zeit Now](https://zeit.co/now) is a great easy to use service for hosting your app that has an easy to use free plan for small projects.
|
||||
|
||||
To deploy your GraphQL service on Zeit Now, first install [Now Desktop](https://zeit.co/download) - you'll need to provide an email address. Then run
|
||||
|
||||
```
|
||||
now
|
||||
```
|
||||
|
||||
to deploy your GraphQL service on Zeit Now. Once deployed you'll be given a fresh URL that represents the current state of your application where you can access your GraphQL endpoint and GraphQL Playgound. For example: https://grand-stack-starter-api-pqdeodpvok.now.sh/
|
||||
|
||||
## Seeding The Database
|
||||
|
||||
Optionally you can seed the GraphQL service by executing mutations that will write sample data to the database:
|
||||
|
||||
```
|
||||
npm run seedDb
|
||||
```
|
|
@ -1,11 +0,0 @@
|
|||
# Copy this file to .env
|
||||
|
||||
NEO4J_URI=bolt://localhost:7687
|
||||
NEO4J_USER=neo4j
|
||||
NEO4J_PASSWORD=letmein
|
||||
|
||||
GRAPHQL_LISTEN_PORT=4001
|
||||
API_ENDPOINT=http://localhost:4001/
|
||||
|
||||
JWT_SECRET=You-sh0uld_Change-Th1s
|
||||
ENCRYPTION_SALT_ROUNDS=12
|
File diff suppressed because it is too large
Load Diff
|
@ -1,73 +0,0 @@
|
|||
{
|
||||
"name": "photoview-api",
|
||||
"version": "0.0.1",
|
||||
"description": "API app for GRANDstack",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start-dev": "./node_modules/.bin/nodemon --exec babel-node src/index.js",
|
||||
"build": "babel src -d build; cp .env build; cp src/schema.graphql build",
|
||||
"now-build": "babel src -d build; cp .env build; cp src/schema.graphql build",
|
||||
"start": "npm run build && node build/index.js",
|
||||
"seedDb": "./node_modules/.bin/babel-node src/seed/seed-db.js"
|
||||
},
|
||||
"author": "Viktor Strate",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@babel/node": "^7.8.3",
|
||||
"@babel/plugin-transform-spread": "^7.8.3",
|
||||
"apollo-boost": "^0.4.7",
|
||||
"apollo-cache-inmemory": "^1.6.5",
|
||||
"apollo-client": "^2.6.8",
|
||||
"apollo-link-http": "^1.5.16",
|
||||
"apollo-server": "^2.9.16",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bcrypt": "^3.0.7",
|
||||
"body-parser": "^1.19.0",
|
||||
"core-js": "^3.6.4",
|
||||
"dotenv": "^8.2.0",
|
||||
"exiftool-vendored": "^9.5.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"graphql-tag": "^2.10.1",
|
||||
"image-size": "^0.8.3",
|
||||
"image-type": "^4.1.0",
|
||||
"is-image": "^3.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.15",
|
||||
"neo4j-driver": "^1.7.6",
|
||||
"neo4j-graphql-js": "^2.11.5",
|
||||
"node-fetch": "^2.3.0",
|
||||
"read-chunk": "^3.2.0",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"sharp": "^0.24.0",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.3",
|
||||
"@babel/core": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.3",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"eslint": "^6.8.0",
|
||||
"husky": "^4.0.10",
|
||||
"lint-staged": "^10.0.0",
|
||||
"nodemon": "^2.0.2",
|
||||
"prettier": "^1.19.1"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,json,css,md,graphql": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import path from 'path'
|
||||
|
||||
// set environment variables from ../.env
|
||||
require('dotenv').config()
|
||||
|
||||
let encryptionSaltRounds = 10
|
||||
if (parseInt(process.env.ENCRYPTION_SALT_ROUNDS)) {
|
||||
encryptionSaltRounds = parseInt(process.env.ENCRYPTION_SALT_ROUNDS)
|
||||
}
|
||||
|
||||
export default {
|
||||
cachePath: process.env.PHOTO_CACHE || path.resolve(__dirname, 'cache'),
|
||||
host: new URL(process.env.API_ENDPOINT || 'http://localhost:4001/'),
|
||||
encryptionSaltRounds,
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { makeAugmentedSchema } from 'neo4j-graphql-js'
|
||||
import _ from 'lodash'
|
||||
|
||||
import usersResolver from './resolvers/users'
|
||||
import scannerResolver from './resolvers/scanner'
|
||||
import photosResolver from './resolvers/photos'
|
||||
import siteInfoResolver from './resolvers/siteInfo'
|
||||
import sharingResolver from './resolvers/sharing'
|
||||
|
||||
const resolvers = [
|
||||
usersResolver,
|
||||
scannerResolver,
|
||||
photosResolver,
|
||||
siteInfoResolver,
|
||||
sharingResolver,
|
||||
]
|
||||
|
||||
const typeDefs = fs
|
||||
.readFileSync(
|
||||
process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql')
|
||||
)
|
||||
.toString('utf-8')
|
||||
|
||||
let productionExcludes = []
|
||||
|
||||
// if (process.env.PRODUCTION == true) {
|
||||
// productionExcludes = [
|
||||
// 'ScannerResult',
|
||||
// 'AuthorizeResult',
|
||||
// 'PhotoURL',
|
||||
// 'SiteInfo',
|
||||
// 'User',
|
||||
// 'Album',
|
||||
// 'PhotoEXIF',
|
||||
// 'Photo',
|
||||
// 'ShareToken',
|
||||
// 'Result',
|
||||
// ]
|
||||
// }
|
||||
|
||||
const schema = makeAugmentedSchema({
|
||||
typeDefs,
|
||||
config: {
|
||||
auth: {
|
||||
isAuthenticated: true,
|
||||
hasRole: true,
|
||||
},
|
||||
mutation: false,
|
||||
query: {
|
||||
exclude: [...productionExcludes],
|
||||
},
|
||||
},
|
||||
resolvers: resolvers.reduce((prev, curr) => _.merge(prev, curr), {}),
|
||||
})
|
||||
|
||||
export default schema
|
|
@ -1,7 +0,0 @@
|
|||
import uuid from 'uuid'
|
||||
|
||||
function generateID() {
|
||||
return uuid().substr(-12)
|
||||
}
|
||||
|
||||
export default generateID
|
|
@ -1,151 +0,0 @@
|
|||
import { ApolloServer } from 'apollo-server-express'
|
||||
import express, { Router } from 'express'
|
||||
import bodyParser from 'body-parser'
|
||||
import cors from 'cors'
|
||||
import { v1 as neo4j } from 'neo4j-driver'
|
||||
import http from 'http'
|
||||
import PhotoScanner from './scanner/Scanner'
|
||||
import _ from 'lodash'
|
||||
import config from './config'
|
||||
import gql from 'graphql-tag'
|
||||
import path from 'path'
|
||||
|
||||
import { getUserFromToken, getTokenFromBearer } from './token'
|
||||
|
||||
const app = express()
|
||||
app.use(bodyParser.json())
|
||||
app.use(cors())
|
||||
|
||||
/*
|
||||
* Create a Neo4j driver instance to connect to the database
|
||||
* using credentials specified as environment variables
|
||||
* with fallback to defaults
|
||||
*/
|
||||
const driver = neo4j.driver(
|
||||
process.env.NEO4J_URI || 'bolt://localhost:7687',
|
||||
neo4j.auth.basic(
|
||||
process.env.NEO4J_USER || 'neo4j',
|
||||
process.env.NEO4J_PASSWORD || 'letmein'
|
||||
)
|
||||
)
|
||||
|
||||
const scanner = new PhotoScanner(driver)
|
||||
|
||||
app.use((req, res, next) => {
|
||||
req.driver = driver
|
||||
req.scanner = scanner
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// Every 4th hour
|
||||
setInterval(scanner.scanAll, 1000 * 60 * 60 * 4)
|
||||
|
||||
// Specify port and path for GraphQL endpoint
|
||||
const graphPath = new URL(path.join(config.host.toString(), '/graphql'))
|
||||
.pathname
|
||||
|
||||
app.use(graphPath, (req, res, next) => {
|
||||
if (req.body.query) {
|
||||
const query = gql(req.body.query)
|
||||
const defs = query.definitions.filter(x => x.kind == 'OperationDefinition')
|
||||
|
||||
const selections = defs.reduce((prev, curr) => {
|
||||
return prev.concat(curr.selectionSet.selections)
|
||||
}, [])
|
||||
|
||||
const names = selections.map(x => x.name.value)
|
||||
const illegalNames = names.filter(
|
||||
name => name.substr(0, 1) == name.substr(0, 1).match(/[A-Z]/)
|
||||
)
|
||||
|
||||
if (illegalNames.length > 0) {
|
||||
return res
|
||||
.status(403)
|
||||
.send({ error: `Illegal query, types not allowed: ${illegalNames}` })
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
const endpointUrl = new URL(config.host)
|
||||
// endpointUrl.port = process.env.GRAPHQL_LISTEN_PORT || 4001
|
||||
|
||||
/*
|
||||
* Create a new ApolloServer instance, serving the GraphQL schema
|
||||
* created using makeAugmentedSchema above and injecting the Neo4j driver
|
||||
* instance into the context object so it is available in the
|
||||
* generated resolvers to connect to the database.
|
||||
*/
|
||||
|
||||
import schema from './graphql-schema'
|
||||
|
||||
const server = new ApolloServer({
|
||||
context: async function({ req }) {
|
||||
let user = null
|
||||
let token = null
|
||||
|
||||
if (req && req.headers.authorization) {
|
||||
token = getTokenFromBearer(req.headers.authorization)
|
||||
user = await getUserFromToken(token, driver)
|
||||
}
|
||||
|
||||
return {
|
||||
...req,
|
||||
driver,
|
||||
scanner,
|
||||
user,
|
||||
token,
|
||||
endpoint: endpointUrl.toString(),
|
||||
}
|
||||
},
|
||||
schema,
|
||||
introspection: true,
|
||||
playground: !process.env.PRODUCTION,
|
||||
subscriptions: {
|
||||
path: graphPath,
|
||||
onConnect: async (connectionParams, webSocket) => {
|
||||
const token = getTokenFromBearer(connectionParams.Authorization)
|
||||
const user = await getUserFromToken(token, driver)
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
server.applyMiddleware({ app, path: graphPath })
|
||||
const router = new Router()
|
||||
|
||||
import loadImageRoutes from './routes/images'
|
||||
import loadDownloadRoutes from './routes/downloads'
|
||||
|
||||
loadImageRoutes(router)
|
||||
loadDownloadRoutes(router)
|
||||
|
||||
app.use(config.host.pathname, router)
|
||||
|
||||
const httpServer = http.createServer(app)
|
||||
server.installSubscriptionHandlers(httpServer)
|
||||
|
||||
httpServer.listen(
|
||||
{ port: process.env.GRAPHQL_LISTEN_PORT, path: graphPath },
|
||||
() => {
|
||||
console.log(
|
||||
`🚀 GraphQL endpoint ready at ${new URL(server.graphqlPath, endpointUrl)}`
|
||||
)
|
||||
|
||||
let subscriptionUrl = new URL(endpointUrl)
|
||||
subscriptionUrl.protocol = 'ws'
|
||||
|
||||
console.log(
|
||||
`🚀 Subscriptions ready at ${new URL(
|
||||
server.subscriptionsPath,
|
||||
endpointUrl
|
||||
)}`
|
||||
)
|
||||
}
|
||||
)
|
|
@ -1,12 +0,0 @@
|
|||
import { cypherQuery } from 'neo4j-graphql-js'
|
||||
|
||||
// Helper functions, that makes it easier to manipulate neo4j-graphql-js translations
|
||||
|
||||
export function replaceMatch({ root, args, ctx, info }, match) {
|
||||
let query = cypherQuery(args, ctx, info)[0]
|
||||
|
||||
query = query.substr(query.indexOf(')') + 1)
|
||||
query = match + query
|
||||
|
||||
return query
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
import { cypherQuery } from 'neo4j-graphql-js'
|
||||
import path from 'path'
|
||||
import config from '../config'
|
||||
|
||||
function injectAt(query, index, injection) {
|
||||
return query.substr(0, index) + injection + query.substr(index)
|
||||
}
|
||||
|
||||
const myAlbumQuery = function(args, ctx, info) {
|
||||
const query = cypherQuery(args, ctx, info)
|
||||
|
||||
const whereSplit = query[0].indexOf('RETURN')
|
||||
|
||||
query[0] = injectAt(
|
||||
query[0],
|
||||
whereSplit,
|
||||
`MATCH (u:User { id: {userid} }) WHERE (u)-[:OWNS]->(album) `
|
||||
)
|
||||
query[1].userid = ctx.user.id
|
||||
|
||||
const addIDSplitIndex = query[0].indexOf('album_photos {')
|
||||
|
||||
if (addIDSplitIndex != -1) {
|
||||
const addIDSplit = addIDSplitIndex + 14
|
||||
|
||||
console.log('ID SPLIT', query[0])
|
||||
|
||||
query[0] = injectAt(
|
||||
query[0],
|
||||
addIDSplit,
|
||||
query[0].indexOf('album_photos {}') == -1 ? ` .id, ` : ` .id `
|
||||
)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
const myPhotoQuery = function(args, ctx, info) {
|
||||
const query = cypherQuery(args, ctx, info)
|
||||
|
||||
const whereSplit = query[0].indexOf('RETURN')
|
||||
|
||||
query[0] = injectAt(
|
||||
query[0],
|
||||
whereSplit,
|
||||
`MATCH (u:User { id: {userid} }) WHERE (u)-[:OWNS]->(:Album)-[:CONTAINS]->(photo) `
|
||||
)
|
||||
query[1].userid = ctx.user.id
|
||||
|
||||
query[0] = injectAt(
|
||||
query[0],
|
||||
query[0].indexOf('RETURN `photo` {') + 16,
|
||||
query[0].indexOf('RETURN `photo` {}') == -1 ? ` .id, ` : ` .id `
|
||||
)
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
const Query = {
|
||||
async myAlbums(root, args, ctx, info) {
|
||||
let query = myAlbumQuery(args, ctx, info)
|
||||
console.log(query)
|
||||
|
||||
const session = ctx.driver.session()
|
||||
|
||||
const result = await session.run(...query)
|
||||
|
||||
session.close()
|
||||
|
||||
return result.records.map(record => record.get('album'))
|
||||
},
|
||||
async album(root, args, ctx, info) {
|
||||
const session = ctx.driver.session()
|
||||
|
||||
let query = myAlbumQuery(args, ctx, info)
|
||||
|
||||
const whereSplit = query[0].indexOf('RETURN')
|
||||
|
||||
query[0] = injectAt(query[0], whereSplit, ` AND album.id = {id} `)
|
||||
query[1].id = args.id
|
||||
console.log(query)
|
||||
|
||||
const result = await session.run(...query)
|
||||
|
||||
session.close()
|
||||
|
||||
if (result.records.length == 0) {
|
||||
throw new Error('Album was not found')
|
||||
}
|
||||
|
||||
return result.records[0].get('album')
|
||||
},
|
||||
async myPhotos(root, args, ctx, info) {
|
||||
let query = myPhotoQuery(args, ctx, info)
|
||||
console.log(query)
|
||||
|
||||
const session = ctx.driver.session()
|
||||
|
||||
const result = await session.run(...query)
|
||||
|
||||
session.close()
|
||||
|
||||
return result.records.map(record => record.get('photo'))
|
||||
},
|
||||
async photo(root, args, ctx, info) {
|
||||
const session = ctx.driver.session()
|
||||
|
||||
let query = myPhotoQuery(args, ctx, info)
|
||||
|
||||
const whereSplit = query[0].indexOf('RETURN')
|
||||
|
||||
query[0] = injectAt(query[0], whereSplit, ` AND photo.id = {id} `)
|
||||
query[1].id = args.id
|
||||
console.log(query)
|
||||
|
||||
const result = await session.run(...query)
|
||||
|
||||
session.close()
|
||||
|
||||
if (result.records.length == 0) {
|
||||
throw new Error('Album was not found')
|
||||
}
|
||||
|
||||
return result.records[0].get('photo')
|
||||
},
|
||||
}
|
||||
|
||||
const urlResolve = {
|
||||
url(root, args, ctx, info) {
|
||||
let url = new URL(path.join(config.host.href, root.url))
|
||||
|
||||
if (ctx.shareToken) url.search = `?token=${ctx.shareToken}`
|
||||
return url.href
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
Query,
|
||||
PhotoURL: urlResolve,
|
||||
PhotoDownload: urlResolve,
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
import { EVENT_SCANNER_PROGRESS } from '../scanner/Scanner'
|
||||
|
||||
const Mutation = {
|
||||
async scanAll(root, args, ctx, info) {
|
||||
ctx.scanner.scanAll()
|
||||
|
||||
return {
|
||||
finished: false,
|
||||
success: true,
|
||||
progress: 0,
|
||||
message: 'Starting scanner',
|
||||
}
|
||||
},
|
||||
async scanUser(root, args, ctx, info) {
|
||||
const session = ctx.driver.session()
|
||||
|
||||
const userResult = await session.run(
|
||||
`MATCH (u:User { id: {userId} }) RETURN u`,
|
||||
{
|
||||
userId: args.userId,
|
||||
}
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
if (userResult.records.length == 0) {
|
||||
return {
|
||||
finished: false,
|
||||
success: false,
|
||||
progress: 0,
|
||||
message: 'Could not scan user: User not found',
|
||||
}
|
||||
}
|
||||
|
||||
const user = userResult.records[0].get('u').properties
|
||||
|
||||
ctx.scanner.scanUser(user)
|
||||
|
||||
return {
|
||||
finished: false,
|
||||
success: true,
|
||||
progress: 0,
|
||||
message: 'Starting scanner',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const Subscription = {
|
||||
scannerStatusUpdate: {
|
||||
subscribe(root, args, ctx, info) {
|
||||
return ctx.scanner.pubsub.asyncIterator([EVENT_SCANNER_PROGRESS])
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation,
|
||||
Subscription,
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import generateID from '../id-generator'
|
||||
import { replaceMatch } from './neo4j-helpers'
|
||||
|
||||
const Mutation = {
|
||||
async shareAlbum(root, args, ctx, info) {
|
||||
const session = ctx.driver.session()
|
||||
|
||||
const ownsAlbumResult = await session.run(
|
||||
`
|
||||
MATCH (u:User { id: {userId} })-[:OWNS]->(a:Album { id: {albumId} })
|
||||
RETURN a
|
||||
`,
|
||||
{
|
||||
userId: ctx.user.id,
|
||||
albumId: args.albumId,
|
||||
}
|
||||
)
|
||||
|
||||
if (ownsAlbumResult.records.length == 0) {
|
||||
session.close()
|
||||
throw new Error('User does not own that album')
|
||||
}
|
||||
|
||||
const createResult = await session.run(
|
||||
`
|
||||
MATCH (u:User { id: {userId} })
|
||||
MATCH (a:Album { id: {albumId} })
|
||||
CREATE (share:ShareToken {shareToken} )
|
||||
CREATE (u)-[:SHARE_TOKEN]->(share)-[:SHARES]->(a)
|
||||
RETURN share
|
||||
`,
|
||||
{
|
||||
userId: ctx.user.id,
|
||||
albumId: args.albumId,
|
||||
shareToken: {
|
||||
token: generateID(),
|
||||
expire: args.expire,
|
||||
password: args.password,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
return {
|
||||
expire: null,
|
||||
password: null,
|
||||
...createResult.records[0].get('share').properties,
|
||||
}
|
||||
},
|
||||
async sharePhoto(root, args, ctx, info) {
|
||||
const session = ctx.driver.session()
|
||||
|
||||
const ownsPhotoResult = await session.run(
|
||||
`
|
||||
MATCH (u:User { id: {userId} })-[:OWNS]->(a:Album)-[:CONTAINS]->(p:Photo { id: {photoId} })
|
||||
RETURN a
|
||||
`,
|
||||
{
|
||||
userId: ctx.user.id,
|
||||
photoId: args.photoId,
|
||||
}
|
||||
)
|
||||
|
||||
if (ownsPhotoResult.records.length == 0) {
|
||||
session.close()
|
||||
throw new Error('User does not own that photo')
|
||||
}
|
||||
|
||||
const createResult = await session.run(
|
||||
`
|
||||
MATCH (u:User { id: {userId} })
|
||||
MATCH (p:Photo { id: {photoId} })
|
||||
CREATE (share:ShareToken {shareToken} )
|
||||
CREATE (u)-[:SHARE_TOKEN]->(share)-[:SHARES]->(p)
|
||||
RETURN share
|
||||
`,
|
||||
{
|
||||
userId: ctx.user.id,
|
||||
photoId: args.photoId,
|
||||
shareToken: {
|
||||
token: generateID(),
|
||||
expire: args.expire,
|
||||
password: args.password,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
return {
|
||||
expire: null,
|
||||
password: null,
|
||||
...createResult.records[0].get('share').properties,
|
||||
}
|
||||
},
|
||||
async deleteShareToken(root, args, ctx, info) {
|
||||
if (!ctx.user.admin) {
|
||||
const session = ctx.driver.session()
|
||||
const result = await session.run(
|
||||
`MATCH (u:User { id: {userId} })-[:SHARE_TOKEN]->(token:ShareToken { token: {token} })
|
||||
RETURN token`,
|
||||
{
|
||||
userId: ctx.user.id,
|
||||
token: args.token,
|
||||
}
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
if (result.records.length == 0) {
|
||||
throw new Error('User is not allowed to delete this share')
|
||||
}
|
||||
}
|
||||
|
||||
return neo4jgraphql(root, args, ctx, info)
|
||||
},
|
||||
}
|
||||
|
||||
const Query = {
|
||||
async albumShares(root, args, ctx, info) {
|
||||
const query = replaceMatch(
|
||||
{ root, args, ctx, info },
|
||||
`
|
||||
MATCH (u:User { id: {userId} })
|
||||
MATCH (u)-[:OWNS]->(a:Album { id: {albumId} })
|
||||
MATCH (a)<-[:SHARES]-(shareToken:ShareToken)
|
||||
`
|
||||
)
|
||||
|
||||
const session = ctx.driver.session()
|
||||
|
||||
const queryResult = await session.run(query, {
|
||||
...args,
|
||||
userId: ctx.user.id,
|
||||
albumId: args.id,
|
||||
})
|
||||
|
||||
session.close()
|
||||
|
||||
const tokens = queryResult.records.map(token => token.get('shareToken'))
|
||||
|
||||
return tokens
|
||||
},
|
||||
async photoShares(root, args, ctx, info) {
|
||||
const query = replaceMatch(
|
||||
{ root, args, ctx, info },
|
||||
`
|
||||
MATCH (u:User { id: {userId} })
|
||||
MATCH (u)-[:OWNS]->(a:Album)-[:CONTAINS]->(p:Photo {id: {photoId} })
|
||||
MATCH (p)<-[:SHARES]-(shareToken:ShareToken)
|
||||
`
|
||||
)
|
||||
|
||||
const session = ctx.driver.session()
|
||||
|
||||
const queryResult = await session.run(query, {
|
||||
...args,
|
||||
userId: ctx.user.id,
|
||||
photoId: args.id,
|
||||
})
|
||||
|
||||
session.close()
|
||||
|
||||
const tokens = queryResult.records.map(token => token.get('shareToken'))
|
||||
|
||||
return tokens
|
||||
},
|
||||
shareToken(root, args, ctx, info) {
|
||||
ctx.shareToken = args.token
|
||||
return neo4jgraphql(root, args, ctx, info)
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation,
|
||||
Query,
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { registerUser, authorizeUser } from './users'
|
||||
|
||||
async function initialSetup(driver) {
|
||||
const session = driver.session()
|
||||
|
||||
await session.run(
|
||||
`MERGE (info:SiteInfo) ON CREATE SET info = {initialSettings}`,
|
||||
{
|
||||
initialSettings: {
|
||||
initialSetup: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
session.close()
|
||||
}
|
||||
|
||||
const Query = {
|
||||
async siteInfo(root, args, ctx, info) {
|
||||
await initialSetup(ctx.driver)
|
||||
|
||||
return neo4jgraphql(root, args, ctx, info)
|
||||
},
|
||||
}
|
||||
|
||||
const Mutation = {
|
||||
async initialSetupWizard(root, args, ctx, info) {
|
||||
await initialSetup(ctx.driver)
|
||||
|
||||
const session = ctx.driver.session()
|
||||
|
||||
const result = await session.run(`MATCH (i:SiteInfo) RETURN i`)
|
||||
|
||||
const siteInfo = result.records[0].get('i').properties
|
||||
|
||||
if (siteInfo.initialSetup == false) {
|
||||
return {
|
||||
success: false,
|
||||
status: 'Has already been setup',
|
||||
token: null,
|
||||
}
|
||||
}
|
||||
|
||||
const userResult = await registerUser(root, args, ctx, info)
|
||||
|
||||
if (!userResult.success) {
|
||||
return userResult
|
||||
}
|
||||
|
||||
const userId = jwt.decode(userResult.token).id
|
||||
|
||||
await session.run(`MATCH (u:User { id: {id} }) SET u.admin = true`, {
|
||||
id: userId,
|
||||
})
|
||||
|
||||
await session.run(`MATCH (i:SiteInfo) SET i.initialSetup = false`)
|
||||
|
||||
session.close()
|
||||
|
||||
const token = (await authorizeUser(root, args, ctx, info)).token
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: 'Initial setup successful',
|
||||
token,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
Query,
|
||||
Mutation,
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
import jwt from 'jsonwebtoken'
|
||||
import generateID from '../id-generator'
|
||||
import fs from 'fs-extra'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import config from '../config'
|
||||
|
||||
const Mutation = {
|
||||
async authorizeUser(root, args, ctx, info) {
|
||||
console.log('Authorize user')
|
||||
let { username } = args
|
||||
|
||||
let session = ctx.driver.session()
|
||||
|
||||
let result = await session.run(
|
||||
'MATCH (user:User {username: {username}}) RETURN user',
|
||||
{ username }
|
||||
)
|
||||
|
||||
if (result.records.length == 0) {
|
||||
return {
|
||||
success: false,
|
||||
status: 'Username or password was invalid',
|
||||
token: null,
|
||||
}
|
||||
}
|
||||
|
||||
const record = result.records[0]
|
||||
|
||||
const user = record.get('user').properties
|
||||
|
||||
if ((await bcrypt.compare(args.password, user.password)) == false) {
|
||||
return {
|
||||
success: false,
|
||||
status: 'Username or password was invalid',
|
||||
token: null,
|
||||
}
|
||||
}
|
||||
|
||||
let roles = []
|
||||
|
||||
if (user.admin) {
|
||||
roles.push('admin')
|
||||
}
|
||||
|
||||
const token = jwt.sign({ id: user.id, roles }, process.env.JWT_SECRET)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: 'Authorized',
|
||||
token,
|
||||
}
|
||||
},
|
||||
async registerUser(root, args, ctx, info) {
|
||||
let { username, rootPath } = args
|
||||
|
||||
let session = ctx.driver.session()
|
||||
let findResult = await session.run(
|
||||
'MATCH (usr:User {username: {username} }) RETURN usr',
|
||||
{ username }
|
||||
)
|
||||
|
||||
if (findResult.records.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
status: 'Username is already taken',
|
||||
token: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await fs.exists(rootPath))) {
|
||||
return {
|
||||
success: false,
|
||||
status: 'Root path does not exist on the server',
|
||||
token: null,
|
||||
}
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(
|
||||
args.password,
|
||||
config.encryptionSaltRounds
|
||||
)
|
||||
|
||||
const registerResult = await session.run(
|
||||
'CREATE (n:User { username: {username}, password: {password}, id: {id}, admin: false, rootPath: {rootPath} }) return n.id',
|
||||
{ username, password: hashedPassword, id: generateID(), rootPath }
|
||||
)
|
||||
|
||||
let id = registerResult.records[0].get('n.id')
|
||||
|
||||
const token = jwt.sign({ id, roles: [] }, process.env.JWT_SECRET)
|
||||
|
||||
session.close()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: 'User created',
|
||||
token: token,
|
||||
}
|
||||
},
|
||||
async updateUser(root, args, ctx, info) {
|
||||
if (args.rootPath) {
|
||||
if (!(await fs.exists(args.rootPath))) {
|
||||
throw Error('New root path not found in server filesystem')
|
||||
}
|
||||
}
|
||||
|
||||
if (args.password)
|
||||
args.password = await bcrypt.hash(
|
||||
args.password,
|
||||
config.encryptionSaltRounds
|
||||
)
|
||||
|
||||
return neo4jgraphql(root, args, ctx, info)
|
||||
},
|
||||
async createUser(root, args, ctx, info) {
|
||||
if (args.rootPath) {
|
||||
if (!(await fs.exists(args.rootPath))) {
|
||||
throw Error('Root path not found in server filesystem')
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
args.id = generateID()
|
||||
|
||||
if (args.password)
|
||||
args.password = await bcrypt.hash(
|
||||
args.password,
|
||||
config.encryptionSaltRounds
|
||||
)
|
||||
|
||||
return neo4jgraphql(root, args, ctx, info)
|
||||
},
|
||||
async changeUserPassword(root, args, ctx, info) {
|
||||
const { newPassword, id } = args
|
||||
|
||||
const session = ctx.driver.session()
|
||||
|
||||
const hashedPassword = await bcrypt.hash(
|
||||
newPassword,
|
||||
config.encryptionSaltRounds
|
||||
)
|
||||
|
||||
await session.run(
|
||||
`MATCH (u:User { id: {id} }) SET u.password = {password}`,
|
||||
{
|
||||
id,
|
||||
password: hashedPassword,
|
||||
}
|
||||
)
|
||||
|
||||
session.close
|
||||
|
||||
return {
|
||||
success: true,
|
||||
errorMessage: null,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const registerUser = Mutation.registerUser
|
||||
export const authorizeUser = Mutation.authorizeUser
|
||||
|
||||
const Query = {
|
||||
myUser(root, args, ctx, info) {
|
||||
let customArgs = {
|
||||
filter: {},
|
||||
...args,
|
||||
}
|
||||
|
||||
customArgs.filter.id = ctx.user.id
|
||||
|
||||
return neo4jgraphql(root, customArgs, ctx, info)
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation,
|
||||
Query,
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { getImageFromRequest, RequestError } from './images'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
|
||||
async function sendDownload(req, res) {
|
||||
let { photo, cachePath } = req
|
||||
const cacheBasename = path.basename(cachePath)
|
||||
const photoBasename = path.basename(photo.path)
|
||||
|
||||
if (cacheBasename == photoBasename) {
|
||||
if (!(await fs.exists(photo.path))) {
|
||||
throw new RequestError(500, 'Image missing from the server')
|
||||
}
|
||||
|
||||
return res.sendFile(photo.path)
|
||||
}
|
||||
|
||||
throw new RequestError(404, 'Image could not be found')
|
||||
}
|
||||
|
||||
const loadDownloadRoutes = router => {
|
||||
router.use('/download/:id/:image', getImageFromRequest)
|
||||
router.use('/download/:id/:image', sendDownload)
|
||||
}
|
||||
|
||||
export default loadDownloadRoutes
|
|
@ -1,185 +0,0 @@
|
|||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import _ from 'lodash'
|
||||
import config from '../config'
|
||||
import { isRawImage, getImageCachePath } from '../scanner/utils'
|
||||
import { getUserFromToken, getTokenFromBearer } from '../token'
|
||||
|
||||
export class RequestError extends Error {
|
||||
constructor(httpCode, message) {
|
||||
super(message)
|
||||
this.httpCode = httpCode
|
||||
}
|
||||
}
|
||||
|
||||
export async function getImageFromRequest(req, res, next) {
|
||||
const { id, image } = req.params
|
||||
const driver = req.driver
|
||||
|
||||
const shareToken = req.query.token
|
||||
|
||||
let photo, albumId
|
||||
|
||||
try {
|
||||
let verify = null
|
||||
|
||||
if (shareToken) {
|
||||
verify = await verifyShareToken({ shareToken, id, driver })
|
||||
}
|
||||
|
||||
if (!verify) {
|
||||
verify = await verifyUser(req, id)
|
||||
}
|
||||
|
||||
if (verify == null) throw RequestError(500, 'Unable to verify request')
|
||||
|
||||
photo = verify.photo
|
||||
albumId = verify.albumId
|
||||
} catch (error) {
|
||||
return res.status(error.status || 500).send(error.message)
|
||||
}
|
||||
|
||||
let cachePath = path.resolve(getImageCachePath(id, albumId), image)
|
||||
req.cachePath = cachePath
|
||||
req.photo = photo
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
async function sendImage(req, res) {
|
||||
let { photo, cachePath } = req
|
||||
const photoBasename = path.basename(cachePath)
|
||||
|
||||
if (!(await fs.exists(cachePath))) {
|
||||
if (photoBasename == 'thumbnail.jpg') {
|
||||
console.log('Thumbnail not found, generating', photo.path)
|
||||
await req.scanner.processImage(photo.id)
|
||||
|
||||
if (!(await fs.exists(cachePath))) {
|
||||
throw new Error('Thumbnail not found after image processing')
|
||||
}
|
||||
|
||||
return res.sendFile(cachePath)
|
||||
}
|
||||
|
||||
cachePath = photo.path
|
||||
}
|
||||
|
||||
if (await isRawImage(cachePath)) {
|
||||
console.log('RAW preview image not found, generating', cachePath)
|
||||
await req.scanner.processImage(photo.id)
|
||||
|
||||
cachePath = path.resolve(
|
||||
config.cachePath,
|
||||
'images',
|
||||
photo.id,
|
||||
photoBasename
|
||||
)
|
||||
|
||||
if (!(await fs.exists(cachePath))) {
|
||||
throw new Error('RAW preview not found after image processing')
|
||||
}
|
||||
|
||||
return res.sendFile(cachePath)
|
||||
}
|
||||
|
||||
res.sendFile(cachePath)
|
||||
}
|
||||
|
||||
async function verifyUser(req, id) {
|
||||
let user = null
|
||||
const { driver } = req
|
||||
|
||||
try {
|
||||
const token = getTokenFromBearer(req.headers.authorization)
|
||||
user = await getUserFromToken(token, driver)
|
||||
} catch (err) {
|
||||
throw new RequestError(401, err.message)
|
||||
// return res.status(401).send(err.message)
|
||||
}
|
||||
|
||||
const session = driver.session()
|
||||
|
||||
const result = await session.run(
|
||||
'MATCH (p:Photo { id: {id} })<-[:CONTAINS]-(a:Album)<-[:OWNS]-(u:User) RETURN p as photo, u.id as userId, a.id as albumId',
|
||||
{
|
||||
id,
|
||||
}
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
if (result.records.length == 0) {
|
||||
throw new RequestError(404, 'Image not found')
|
||||
// return res.status(404).send(`Image not found`)
|
||||
}
|
||||
|
||||
const userId = result.records[0].get('userId')
|
||||
const albumId = result.records[0].get('albumId')
|
||||
const photo = result.records[0].get('photo').properties
|
||||
|
||||
if (userId != user.id) {
|
||||
throw new RequestError(401, 'Image not owned by you')
|
||||
// return res.status(401).send(`Image not owned by you`)
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
albumId,
|
||||
photo,
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyShareToken({ shareToken, id, driver }) {
|
||||
const session = driver.session()
|
||||
|
||||
const shareTokenResult = await session.run(
|
||||
`MATCH (share:ShareToken { token: {shareToken} })-[:SHARES]->(shared)
|
||||
MATCH (photo:Photo { id: {id} })<-[:CONTAINS]-(album:Album)
|
||||
RETURN share, photo, shared, album`,
|
||||
{ shareToken, id }
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
if (shareTokenResult.records.length == 0) {
|
||||
throw new RequestError(404, 'Image not found')
|
||||
}
|
||||
|
||||
const share = shareTokenResult.records[0].get('share').properties
|
||||
const album = shareTokenResult.records[0].get('album').properties
|
||||
const photo = shareTokenResult.records[0].get('photo').properties
|
||||
const sharedObject = shareTokenResult.records[0].get('shared')
|
||||
|
||||
if (sharedObject.labels[0] == 'Album') {
|
||||
const session = driver.session()
|
||||
const albumResult = await session.run(
|
||||
`MATCH (album)-[:CONTAINS]->(photo:Photo { id: {id} })
|
||||
RETURN album`,
|
||||
{ id }
|
||||
)
|
||||
session.close()
|
||||
|
||||
if (albumResult.records.length == 0) {
|
||||
throw new RequestError(403, 'Invalid share token')
|
||||
}
|
||||
} else {
|
||||
const sharedPhoto = sharedObject.properties
|
||||
|
||||
if (sharedPhoto.id != photo.id) {
|
||||
throw new RequestError(403, 'Invalid share token')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
photo,
|
||||
albumId: album.id,
|
||||
}
|
||||
}
|
||||
|
||||
function loadImageRoutes(router) {
|
||||
router.use('/images/:id/:image', getImageFromRequest)
|
||||
router.use('/images/:id/:image', sendImage)
|
||||
}
|
||||
|
||||
export default loadImageRoutes
|
|
@ -1,146 +0,0 @@
|
|||
import { PubSub } from 'apollo-server'
|
||||
import _ from 'lodash'
|
||||
import _scanUser from './scanUser'
|
||||
import _scanAlbum from './scanAlbum'
|
||||
import _processImage from './processImage'
|
||||
import _scanAll from './scanAll'
|
||||
|
||||
export const EVENT_SCANNER_PROGRESS = 'SCANNER_PROGRESS'
|
||||
|
||||
async function _execScan(scanner, scanFunction) {
|
||||
try {
|
||||
if (scanner.isRunning) throw new Error('Scanner already running')
|
||||
scanner.isRunning = true
|
||||
scanner.imageProgress = {}
|
||||
|
||||
const session = scanner.driver.session()
|
||||
const photoResult = await session.run(
|
||||
'MATCH (photo:Photo) RETURN photo.id as photoId'
|
||||
)
|
||||
session.close()
|
||||
|
||||
photoResult.records
|
||||
.map(x => x.get('photoId'))
|
||||
.forEach(id => {
|
||||
scanner.markImageToProgress(id)
|
||||
})
|
||||
|
||||
scanner.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
||||
scannerStatusUpdate: {
|
||||
progress: 0,
|
||||
finished: false,
|
||||
success: true,
|
||||
message: 'Scan started',
|
||||
},
|
||||
})
|
||||
|
||||
console.log('Calling scan function')
|
||||
await scanFunction()
|
||||
console.log('Scan function ended')
|
||||
|
||||
console.log(
|
||||
`Done scanning ${Object.keys(scanner.imageProgress).length} photos`
|
||||
)
|
||||
|
||||
scanner.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
||||
scannerStatusUpdate: {
|
||||
progress: 100,
|
||||
finished: true,
|
||||
success: true,
|
||||
message: `Done scanning ${
|
||||
Object.keys(scanner.imageProgress).length
|
||||
} photos`,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(`SCANNER ERROR: ${e.message}\n${e.stack}`)
|
||||
scanner.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
||||
scannerStatusUpdate: {
|
||||
progress: 0,
|
||||
finished: true,
|
||||
success: false,
|
||||
message: `Scanner error: ${e.message}`,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
scanner.isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
class PhotoScanner {
|
||||
constructor(driver) {
|
||||
this.driver = driver
|
||||
this.isRunning = false
|
||||
this.pubsub = new PubSub()
|
||||
|
||||
this.processImage = this.processImage.bind(this)
|
||||
this.scanAlbum = this.scanAlbum.bind(this)
|
||||
this.scanUser = this.scanUser.bind(this)
|
||||
this.scanAll = this.scanAll.bind(this)
|
||||
|
||||
this.imageProgress = {}
|
||||
|
||||
this.markImageToProgress = imageId => {
|
||||
if (!this.imageProgress[imageId]) this.imageProgress[imageId] = false
|
||||
}
|
||||
|
||||
this.markFinishedImage = imageId => {
|
||||
this.imageProgress[imageId] = true
|
||||
this.broadcastProgress()
|
||||
}
|
||||
|
||||
this.finishedImages = () =>
|
||||
Object.values(this.imageProgress).reduce((prev, x) => {
|
||||
x ? prev++ : prev
|
||||
return prev
|
||||
}, 0)
|
||||
|
||||
this.broadcastProgress = _.throttle(() => {
|
||||
if (!this.isRunning) return
|
||||
if (Object.keys(this.imageProgress).length == 0) return
|
||||
|
||||
let progress =
|
||||
(this.finishedImages() / Object.keys(this.imageProgress).length) * 100
|
||||
|
||||
console.log(`Progress: ${progress}`)
|
||||
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
||||
scannerStatusUpdate: {
|
||||
progress,
|
||||
finished: false,
|
||||
success: true,
|
||||
message: `${this.finishedImages()} photos scanned`,
|
||||
},
|
||||
})
|
||||
}, 250)
|
||||
|
||||
this.markImageToProgress = this.markImageToProgress.bind(this)
|
||||
this.markFinishedImage = this.markFinishedImage.bind(this)
|
||||
this.finishedImages = this.finishedImages.bind(this)
|
||||
}
|
||||
|
||||
async scanUser(user) {
|
||||
await _execScan(this, async () => {
|
||||
await _scanUser(this, user)
|
||||
})
|
||||
}
|
||||
|
||||
async scanAlbum(album) {
|
||||
await _execScan(this, async () => {
|
||||
await _scanAlbum(this, album)
|
||||
})
|
||||
}
|
||||
|
||||
async processImage(id) {
|
||||
await _execScan(this, async () => {
|
||||
await _processImage(this, id)
|
||||
})
|
||||
}
|
||||
|
||||
async scanAll() {
|
||||
await _execScan(this, async () => {
|
||||
await _scanAll(this)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default PhotoScanner
|
|
@ -1,202 +0,0 @@
|
|||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { exiftool } from 'exiftool-vendored'
|
||||
import sharp from 'sharp'
|
||||
import { isRawImage, imageSize, getImageCachePath } from './utils'
|
||||
import { DateTime as NeoDateTime } from 'neo4j-driver/lib/v1/temporal-types.js'
|
||||
|
||||
async function addExifTags({ session, photo }) {
|
||||
const exifResult = await session.run(
|
||||
'MATCH (p:Photo { id: {id} })-[:EXIF]->(exif:PhotoEXIF) RETURN exif',
|
||||
{
|
||||
id: photo.id,
|
||||
}
|
||||
)
|
||||
|
||||
if (exifResult.records.length > 0) return
|
||||
|
||||
const rawTags = await exiftool.read(photo.path)
|
||||
|
||||
let iso = rawTags.ISO
|
||||
if (typeof iso != 'number') {
|
||||
try {
|
||||
iso = parseInt(iso)
|
||||
} catch (e) {
|
||||
console.log('Could not parse ISO as int', e, e.stack)
|
||||
iso = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const photoExif = {
|
||||
camera: rawTags.Model,
|
||||
maker: rawTags.Make,
|
||||
lens: rawTags.LensType,
|
||||
dateShot:
|
||||
rawTags.DateTimeOriginal &&
|
||||
typeof rawTags.DateTimeOriginal.toDate == 'function' &&
|
||||
NeoDateTime.fromStandardDate(rawTags.DateTimeOriginal.toDate()),
|
||||
fileSize: rawTags.FileSize,
|
||||
exposure: rawTags.ShutterSpeedValue,
|
||||
aperture: rawTags.ApertureValue,
|
||||
iso,
|
||||
focalLength: rawTags.FocalLength,
|
||||
flash: rawTags.Flash,
|
||||
}
|
||||
|
||||
const result = await session.run(
|
||||
`MATCH (p:Photo { id: {id} })
|
||||
CREATE (p)-[:EXIF]->(exif:PhotoEXIF {exifProps})`,
|
||||
{
|
||||
id: photo.id,
|
||||
exifProps: photoExif,
|
||||
}
|
||||
)
|
||||
|
||||
console.log('Added exif tags to photo', photo.path)
|
||||
}
|
||||
|
||||
export default async function processImage(scanner, id) {
|
||||
const { driver, markFinishedImage } = scanner
|
||||
const session = driver.session()
|
||||
|
||||
const result = await session.run(
|
||||
`MATCH (p:Photo { id: {id} })<-[:CONTAINS]-(a:Album) RETURN p, a.id`,
|
||||
{
|
||||
id,
|
||||
}
|
||||
)
|
||||
|
||||
const photo = result.records[0].get('p').properties
|
||||
const albumId = result.records[0].get('a.id')
|
||||
|
||||
const imagePath = getImageCachePath(id, albumId)
|
||||
|
||||
// Verify that processing is needed
|
||||
if (await fs.exists(path.resolve(imagePath, 'thumbnail.jpg'))) {
|
||||
const urlResult = await session.run(
|
||||
`MATCH (p:Photo { id: {id} })-->(urls:PhotoURL) RETURN urls`,
|
||||
{ id }
|
||||
)
|
||||
|
||||
if (urlResult.records.length == 2) {
|
||||
markFinishedImage(id)
|
||||
|
||||
session.close()
|
||||
|
||||
console.log('Skipping image', photo.path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Begin processing
|
||||
await session.run(
|
||||
`MATCH (p:Photo { id: {id} })-->(urls:PhotoURL) DETACH DELETE urls`,
|
||||
{ id }
|
||||
)
|
||||
|
||||
try {
|
||||
await fs.remove(imagePath)
|
||||
await fs.mkdirp(imagePath)
|
||||
} catch (e) {
|
||||
console.error('Could not remove old image, and make directory', e, e.stack)
|
||||
}
|
||||
|
||||
let originalPath = photo.path
|
||||
|
||||
if (await isRawImage(photo.path)) {
|
||||
// console.log('Processing RAW image')
|
||||
|
||||
const extractedPath = path.resolve(imagePath, 'extracted.jpg')
|
||||
await exiftool.extractPreview(photo.path, extractedPath)
|
||||
|
||||
const rawTags = await exiftool.read(photo.path)
|
||||
// ISO, FNumber, Model, ExposureTime, FocalLength, LensType
|
||||
// console.log(rawTags)
|
||||
|
||||
let rotateAngle = null
|
||||
switch (rawTags.Orientation) {
|
||||
case 8:
|
||||
rotateAngle = -90
|
||||
break
|
||||
case 3:
|
||||
rotateAngle = 180
|
||||
break
|
||||
case 6:
|
||||
rotateAngle = 90
|
||||
}
|
||||
|
||||
// Replace extension with .jpg
|
||||
let processedBase = path.basename(photo.path).match(/(.*)(\..*)/)
|
||||
processedBase =
|
||||
processedBase == null ? path.basename(photo.path) : processedBase[1]
|
||||
processedBase += '.jpg'
|
||||
|
||||
const processedPath = path.resolve(imagePath, processedBase)
|
||||
await sharp(extractedPath)
|
||||
.jpeg({ quality: 80 })
|
||||
.rotate(rotateAngle)
|
||||
.toFile(processedPath)
|
||||
|
||||
fs.remove(extractedPath)
|
||||
|
||||
originalPath = processedPath
|
||||
}
|
||||
|
||||
// Resize image
|
||||
const thumbnailPath = path.resolve(imagePath, 'thumbnail.jpg')
|
||||
await sharp(originalPath)
|
||||
.jpeg({ quality: 70 })
|
||||
.resize(720, 480, { fit: 'inside', withoutEnlargement: true })
|
||||
.toFile(thumbnailPath)
|
||||
|
||||
try {
|
||||
const { width: originalWidth, height: originalHeight } = await imageSize(
|
||||
originalPath
|
||||
)
|
||||
const { width: thumbnailWidth, height: thumbnailHeight } = await imageSize(
|
||||
thumbnailPath
|
||||
)
|
||||
|
||||
await session.run(
|
||||
`MATCH (p:Photo { id: {id} })
|
||||
CREATE (p)-[:THUMBNAIL_URL]->(thumbnail:PhotoURL { thumbnail })
|
||||
CREATE (p)-[:ORIGINAL_URL]->(original:PhotoURL { original })
|
||||
`,
|
||||
{
|
||||
id,
|
||||
thumbnail: {
|
||||
url: `/images/${id}/${path.basename(thumbnailPath)}`,
|
||||
width: thumbnailWidth,
|
||||
height: thumbnailHeight,
|
||||
},
|
||||
original: {
|
||||
url: `/images/${id}/${path.basename(originalPath)}`,
|
||||
width: originalWidth,
|
||||
height: originalHeight,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
await session.run(
|
||||
`
|
||||
MATCH (p:Photo { id: {id} })
|
||||
CREATE (p)-[:DOWNLOAD]->(original:PhotoDownload {original})
|
||||
`,
|
||||
{
|
||||
id,
|
||||
original: {
|
||||
title: 'Original',
|
||||
url: `/download/${id}/${path.basename(photo.path)}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
console.log('Create photo url failed', e)
|
||||
}
|
||||
|
||||
await addExifTags({ session, photo })
|
||||
|
||||
session.close()
|
||||
|
||||
markFinishedImage(id)
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import generateID from '../id-generator'
|
||||
import { isImage, getImageCachePath } from './utils'
|
||||
import _processImage from './processImage'
|
||||
import { EVENT_SCANNER_PROGRESS } from './Scanner'
|
||||
|
||||
export default async function scanAlbum(scanner, album) {
|
||||
const { driver, markImageToProgress } = scanner
|
||||
const { title, path: albumPath, id } = album
|
||||
|
||||
console.log('Scanning album', title)
|
||||
|
||||
let processedImages = []
|
||||
|
||||
const list = await fs.readdir(albumPath)
|
||||
let processingImagePromises = []
|
||||
|
||||
const addPhotoToProcess = photo => {
|
||||
markImageToProgress(photo.id)
|
||||
|
||||
processingImagePromises.push(
|
||||
_processImage(scanner, photo.id).catch(e => {
|
||||
console.error(
|
||||
`Error processing image (${JSON.stringify(photo)}): ${e.stack}`
|
||||
)
|
||||
scanner.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
||||
scannerStatusUpdate: {
|
||||
progress: 0,
|
||||
finished: false,
|
||||
success: false,
|
||||
message: `Error processing image at ${photo.path}: ${e.message}`,
|
||||
},
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
for (const item of list) {
|
||||
const itemPath = path.resolve(albumPath, item)
|
||||
processedImages.push(itemPath)
|
||||
|
||||
if (await isImage(itemPath)) {
|
||||
const session = driver.session()
|
||||
|
||||
const photoResult = await session.run(
|
||||
`MATCH (p:Photo {path: {imgPath} })<--(a:Album {id: {albumId}}) RETURN p`,
|
||||
{
|
||||
imgPath: itemPath,
|
||||
albumId: id,
|
||||
}
|
||||
)
|
||||
|
||||
if (photoResult.records.length != 0) {
|
||||
// console.log(`Photo already exists ${item}`)
|
||||
|
||||
const photo = photoResult.records[0].get('p').properties
|
||||
|
||||
addPhotoToProcess(photo)
|
||||
} else {
|
||||
console.log(`Found new image at ${itemPath}`)
|
||||
|
||||
const photo = {
|
||||
id: generateID(),
|
||||
path: itemPath,
|
||||
title: item,
|
||||
}
|
||||
|
||||
await session.run(
|
||||
`MATCH (a:Album { id: {albumId} })
|
||||
CREATE (p:Photo {photo})
|
||||
CREATE (a)-[:CONTAINS]->(p)`,
|
||||
{
|
||||
photo,
|
||||
albumId: id,
|
||||
}
|
||||
)
|
||||
|
||||
addPhotoToProcess(photo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const session = driver.session()
|
||||
|
||||
const deletedImagesResult = await session.run(
|
||||
`MATCH (a:Album { id: {albumId} })-[:CONTAINS]->(p:Photo)-->(trail)
|
||||
WHERE NOT p.path IN {images}
|
||||
WITH p, p.id AS imageId, trail
|
||||
DETACH DELETE p, trail
|
||||
RETURN DISTINCT imageId`,
|
||||
{
|
||||
albumId: id,
|
||||
images: processedImages,
|
||||
}
|
||||
)
|
||||
|
||||
const deletedImages = deletedImagesResult.records.map(record =>
|
||||
record.get('imageId')
|
||||
)
|
||||
|
||||
for (const imageId of deletedImages) {
|
||||
await fs.remove(getImageCachePath(imageId, id))
|
||||
}
|
||||
|
||||
console.log(`Deleted ${deletedImages.length} images from album ${title}`)
|
||||
|
||||
session.close()
|
||||
|
||||
await Promise.all(processingImagePromises).catch(e => {
|
||||
console.error(`Error processing image: ${e.stack}`)
|
||||
scanner.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
||||
scannerStatusUpdate: {
|
||||
progress: 0,
|
||||
finished: false,
|
||||
success: false,
|
||||
message: `Error processing image: ${e.message}`,
|
||||
},
|
||||
})
|
||||
})
|
||||
console.log('Done processing album', album.title)
|
||||
|
||||
scanner.broadcastProgress()
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import _scanUser from './scanUser'
|
||||
|
||||
export default function scanAll(scanner) {
|
||||
const { driver } = scanner
|
||||
return new Promise((resolve, reject) => {
|
||||
let session = driver.session()
|
||||
|
||||
let usersToScan = []
|
||||
|
||||
session.run('MATCH (u:User) return u').subscribe({
|
||||
onNext: record => {
|
||||
const user = record.toObject().u.properties
|
||||
|
||||
if (!user.rootPath) {
|
||||
console.log(`User ${user.username}, has no root path, skipping`)
|
||||
return
|
||||
}
|
||||
|
||||
usersToScan.push(user)
|
||||
},
|
||||
onCompleted: async () => {
|
||||
session.close()
|
||||
|
||||
for (let user of usersToScan) {
|
||||
try {
|
||||
await _scanUser(scanner, user)
|
||||
} catch (reason) {
|
||||
console.log(
|
||||
`User scan exception for user ${user.username} ${reason}`
|
||||
)
|
||||
reject(reason)
|
||||
}
|
||||
}
|
||||
|
||||
resolve()
|
||||
},
|
||||
onError: error => {
|
||||
session.close()
|
||||
reject(error)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
import fs from 'fs-extra'
|
||||
import { resolve as pathResolve } from 'path'
|
||||
import generateID from '../id-generator'
|
||||
import { isImage, getAlbumCachePath } from './utils'
|
||||
import _scanAlbum from './scanAlbum'
|
||||
|
||||
export default async function scanUser(scanner, user) {
|
||||
const { driver } = scanner
|
||||
|
||||
console.log('Scanning user', user.username, 'at', user.rootPath)
|
||||
|
||||
let foundAlbumIds = []
|
||||
|
||||
async function scanPath(path) {
|
||||
const list = fs.readdirSync(path)
|
||||
|
||||
let foundImageOrAlbum = false
|
||||
let newAlbums = []
|
||||
|
||||
for (const item of list) {
|
||||
const itemPath = pathResolve(path, item)
|
||||
// console.log(`Scanning item ${itemPath}...`)
|
||||
let stat = null
|
||||
|
||||
try {
|
||||
stat = await fs.stat(itemPath)
|
||||
} catch {
|
||||
console.log('ERROR reading file stat for item:', itemPath)
|
||||
}
|
||||
|
||||
if (stat && stat.isDirectory()) {
|
||||
const session = driver.session()
|
||||
|
||||
const findAlbumResult = await session.run(
|
||||
'MATCH (a:Album { path: {path} }) RETURN a',
|
||||
{
|
||||
path: itemPath,
|
||||
}
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
const {
|
||||
foundImage: imagesInDirectory,
|
||||
newAlbums: childAlbums,
|
||||
} = await scanPath(itemPath)
|
||||
|
||||
if (findAlbumResult.records.length > 0) {
|
||||
const album = findAlbumResult.records[0].toObject().a.properties
|
||||
console.log('Found existing album', album.title)
|
||||
|
||||
foundImageOrAlbum = true
|
||||
foundAlbumIds.push(album.id)
|
||||
await _scanAlbum(scanner, album)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (imagesInDirectory) {
|
||||
console.log(`Found new album at ${itemPath}`)
|
||||
foundImageOrAlbum = true
|
||||
|
||||
const session = driver.session()
|
||||
|
||||
console.log('Adding album')
|
||||
const albumId = generateID()
|
||||
const albumResult = await session.run(
|
||||
`MATCH (u:User { id: {userId} })
|
||||
CREATE (a:Album { id: {id}, title: {title}, path: {path} })
|
||||
CREATE (u)-[own:OWNS]->(a)
|
||||
RETURN a`,
|
||||
{
|
||||
id: albumId,
|
||||
userId: user.id,
|
||||
title: item,
|
||||
path: itemPath,
|
||||
}
|
||||
)
|
||||
|
||||
newAlbums.push(albumId)
|
||||
const album = albumResult.records[0].toObject().a.properties
|
||||
|
||||
console.log(
|
||||
`Linking ${childAlbums.length} child albums for ${album.title}`
|
||||
)
|
||||
for (let childAlbum of childAlbums) {
|
||||
await session.run(
|
||||
`MATCH (parent:Album { id: {parentId} })
|
||||
MATCH (child:Album { id: {childId} })
|
||||
CREATE (parent)-[:SUBALBUM]->(child)`,
|
||||
{
|
||||
parentId: albumId,
|
||||
childId: childAlbum,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
foundAlbumIds.push(album.id)
|
||||
await _scanAlbum(scanner, album)
|
||||
|
||||
session.close()
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (!foundImageOrAlbum && (await isImage(itemPath))) {
|
||||
foundImageOrAlbum = true
|
||||
}
|
||||
}
|
||||
|
||||
return { foundImage: foundImageOrAlbum, newAlbums }
|
||||
}
|
||||
|
||||
await fs.mkdirp(user.rootPath)
|
||||
|
||||
await scanPath(user.rootPath)
|
||||
|
||||
const session = driver.session()
|
||||
|
||||
const userAlbumsResult = await session.run(
|
||||
`MATCH (u:User { id: {userId} })-[:OWNS]->(a:Album)
|
||||
WHERE NOT a.id IN {foundAlbums}
|
||||
OPTIONAL MATCH (a)-[:CONTAINS]->(p:Photo)-->(photoTail)
|
||||
WITH a, p, photoTail, a.id AS albumId
|
||||
DETACH DELETE a, p, photoTail
|
||||
RETURN DISTINCT albumId`,
|
||||
{ userId: user.id, foundAlbums: foundAlbumIds }
|
||||
)
|
||||
|
||||
console.log('FOUND ALBUM IDS', foundAlbumIds)
|
||||
const deletedAlbumIds = userAlbumsResult.records.map(record =>
|
||||
record.get('albumId')
|
||||
)
|
||||
|
||||
for (const albumId of deletedAlbumIds) {
|
||||
try {
|
||||
await fs.remove(getAlbumCachePath(albumId))
|
||||
} catch (e) {
|
||||
console.error('Error while trying to delete album from cache', e, e.stack)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Deleted ${userAlbumsResult.records.length} albums from ${user.username} that was not found locally`
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
console.log('User scan complete')
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import fs from 'fs-extra'
|
||||
import readChunk from 'read-chunk'
|
||||
import imageType from 'image-type'
|
||||
import { promisify } from 'util'
|
||||
import path from 'path'
|
||||
import config from '../config'
|
||||
|
||||
const rawTypes = ['cr2', 'arw', 'crw', 'dng']
|
||||
const imageTypes = [...rawTypes, 'jpg', 'png', 'gif', 'bmp']
|
||||
|
||||
export const isImage = async path => {
|
||||
if ((await fs.stat(path)).isDirectory()) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await readChunk(path, 0, 12)
|
||||
const type = imageType(buffer)
|
||||
|
||||
return type && imageTypes.includes(type.ext)
|
||||
} catch (e) {
|
||||
throw new Error(`isImage error at ${path}: ${JSON.stringify(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const isRawImage = async path => {
|
||||
try {
|
||||
const buffer = await readChunk(path, 0, 12)
|
||||
const { ext } = imageType(buffer)
|
||||
|
||||
return rawTypes.includes(ext)
|
||||
} catch (e) {
|
||||
throw new Error(`isRawImage error at ${path}: ${JSON.stringify(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const imageSize = promisify(require('image-size'))
|
||||
|
||||
export const getAlbumCachePath = id =>
|
||||
path.resolve(config.cachePath, 'images', id)
|
||||
|
||||
export const getImageCachePath = (imageId, albumId) =>
|
||||
path.resolve(getAlbumCachePath(albumId), imageId)
|
|
@ -1,167 +0,0 @@
|
|||
enum Role {
|
||||
admin
|
||||
user
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
username: String!
|
||||
albums: [Album] @relation(name: "OWNS", direction: "OUT")
|
||||
# Local filepath for the user's photos
|
||||
rootPath: String! @hasRole(roles: [admin])
|
||||
admin: Boolean
|
||||
shareTokens: [ShareToken] @relation(name: "SHARE_TOKEN", direction: "OUT")
|
||||
}
|
||||
|
||||
type Album {
|
||||
id: ID!
|
||||
title: String
|
||||
photos: [Photo] @relation(name: "CONTAINS", direction: "OUT")
|
||||
subAlbums: [Album] @relation(name: "SUBALBUM", direction: "OUT")
|
||||
parentAlbum: Album @relation(name: "SUBALBUM", direction: "IN")
|
||||
owner: User! @relation(name: "OWNS", direction: "IN")
|
||||
path: String
|
||||
|
||||
shares: [ShareToken] @relation(name: "SHARES", direction: "IN")
|
||||
}
|
||||
|
||||
type PhotoURL {
|
||||
# URL for previewing the image
|
||||
url: String
|
||||
# Width of the image in pixels
|
||||
width: Int
|
||||
# Height of the image in pixels
|
||||
height: Int
|
||||
}
|
||||
|
||||
type PhotoDownload {
|
||||
title: String
|
||||
url: String
|
||||
}
|
||||
|
||||
type PhotoEXIF {
|
||||
photo: Photo @relation(name: "EXIF", direction: "IN")
|
||||
camera: String
|
||||
maker: String
|
||||
lens: String
|
||||
dateShot: DateTime
|
||||
fileSize: String
|
||||
exposure: String
|
||||
aperture: Float
|
||||
iso: Int
|
||||
focalLength: String
|
||||
flash: String
|
||||
}
|
||||
|
||||
type Photo {
|
||||
id: ID!
|
||||
title: String
|
||||
# Local filepath for the photo
|
||||
path: String
|
||||
# URL to display the photo in full resolution
|
||||
original: PhotoURL @relation(name: "ORIGINAL_URL", direction: "OUT")
|
||||
# URL to display the photo in a smaller resolution
|
||||
thumbnail: PhotoURL @relation(name: "THUMBNAIL_URL", direction: "OUT")
|
||||
# The album that holds the photo
|
||||
album: Album! @relation(name: "CONTAINS", direction: "IN")
|
||||
exif: PhotoEXIF @relation(name: "EXIF", direction: "OUT")
|
||||
|
||||
shares: [ShareToken] @relation(name: "SHARES", direction: "IN")
|
||||
downloads: [PhotoDownload] @relation(name: "DOWNLOAD", direction: "OUT")
|
||||
}
|
||||
|
||||
type ShareToken {
|
||||
token: ID!
|
||||
owner: User @relation(name: "SHARE_TOKEN", direction: "IN")
|
||||
# Optional expire date
|
||||
expire: Date
|
||||
# Optional password
|
||||
# password: String
|
||||
|
||||
album: Album @relation(name: "SHARES", direction: "OUT")
|
||||
photo: Photo @relation(name: "SHARES", direction: "OUT")
|
||||
}
|
||||
|
||||
type SiteInfo {
|
||||
initialSetup: Boolean!
|
||||
}
|
||||
|
||||
type AuthorizeResult {
|
||||
success: Boolean!
|
||||
status: String
|
||||
token: String
|
||||
}
|
||||
|
||||
type ScannerResult {
|
||||
finished: Boolean!
|
||||
success: Boolean!
|
||||
progress: Float
|
||||
message: String
|
||||
}
|
||||
|
||||
type Result {
|
||||
success: Boolean!
|
||||
errorMessage: String
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
scannerStatusUpdate: ScannerResult
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
authorizeUser(username: String!, password: String!): AuthorizeResult!
|
||||
@neo4j_ignore
|
||||
|
||||
registerUser(
|
||||
username: String!
|
||||
password: String!
|
||||
rootPath: String!
|
||||
): AuthorizeResult! @hasRole(roles: [admin]) @neo4j_ignore
|
||||
|
||||
shareAlbum(albumId: ID!, expire: Date, password: String): ShareToken
|
||||
@isAuthenticated
|
||||
sharePhoto(photoId: ID!, expire: Date, password: String): ShareToken
|
||||
@isAuthenticated
|
||||
|
||||
deleteShareToken(token: ID!): ShareToken @isAuthenticated
|
||||
|
||||
setAdmin(userId: ID!, admin: Boolean!): Result!
|
||||
@hasRole(roles: [admin])
|
||||
@neo4j_ignore
|
||||
|
||||
scanAll: ScannerResult! @isAuthenticated @neo4j_ignore
|
||||
scanUser(userId: ID!): ScannerResult! @isAuthenticated @neo4j_ignore
|
||||
|
||||
initialSetupWizard(
|
||||
username: String!
|
||||
password: String!
|
||||
rootPath: String!
|
||||
): AuthorizeResult @neo4j_ignore
|
||||
|
||||
updateUser(id: ID!, username: String, rootPath: String, admin: Boolean): User
|
||||
@hasRole(roles: [admin])
|
||||
createUser(id: ID, username: String, rootPath: String, admin: Boolean): User
|
||||
@hasRole(roles: [admin])
|
||||
deleteUser(id: ID!): User @hasRole(roles: [admin])
|
||||
|
||||
changeUserPassword(id: ID!, newPassword: String!): Result
|
||||
@hasRole(roles: [admin])
|
||||
}
|
||||
|
||||
type Query {
|
||||
siteInfo: SiteInfo
|
||||
|
||||
myUser: User @isAuthenticated
|
||||
user: [User] @hasRole(roles: [admin])
|
||||
|
||||
myAlbums: [Album] @isAuthenticated
|
||||
album(id: ID): Album @isAuthenticated
|
||||
|
||||
myPhotos: [Photo] @isAuthenticated
|
||||
photo(id: ID!): Photo @isAuthenticated
|
||||
|
||||
shareToken(token: ID!): ShareToken
|
||||
|
||||
albumShares(id: ID!, password: String): [ShareToken] @isAuthenticated
|
||||
photoShares(id: ID!, password: String): [ShareToken] @isAuthenticated
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
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 was not found`)
|
||||
}
|
||||
|
||||
let user = userResult.records[0].toObject().u.properties
|
||||
|
||||
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
|
||||
}
|
Loading…
Reference in New Issue