1
Fork 0

Delete folder `api-node-old`

Old api can be found on branch `old-nodejs-server`
This commit is contained in:
viktorstrate 2020-02-10 12:12:34 +01:00
parent 6e72caf2f0
commit 2bbb1740ba
28 changed files with 0 additions and 11548 deletions

View File

@ -1,12 +0,0 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
],
"plugins": ["@babel/plugin-transform-spread"]
}

View File

@ -1,2 +0,0 @@
node_modules/
build/

View File

@ -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"
}

View File

@ -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"]

View File

@ -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
```

View File

@ -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

View File

@ -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"
]
}
}

View File

@ -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,
}

View File

@ -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

View File

@ -1,7 +0,0 @@
import uuid from 'uuid'
function generateID() {
return uuid().substr(-12)
}
export default generateID

View File

@ -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
)}`
)
}
)

View File

@ -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
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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)
},
})
})
}

View File

@ -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')
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}