Commit deebbfc9 authored by jgutix's avatar jgutix

Adding api folder, necessary for running `npm run setup`. Copied from pubsweet/core

parent 9b5ffb07
{
"globals": {
"db": true,
"acl": true
}
}
const express = require('express')
const path = require('path')
const logger = require('morgan')
const cookieParser = require('cookie-parser')
const bodyParser = require('body-parser')
const webpack = require('webpack')
const index = require('./routes/index')
const api = require('./routes/api')
const passport = require('passport')
const jwt = require('jsonwebtoken')
const config = require('../config')
const User = require('./models/User')
// const favicon = require('serve-favicon')
const app = express()
global.versions = {}
// uncomment after placing your favicon in /public
// app.use(favicon (path.join(__dirname, 'public', 'favicon.ico')))
app.use(logger('dev'))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
// Webpack development support
if (process.env.NODE_ENV === 'dev') {
var webpackConfig = require('../webpack/webpack.dev.config.js')
var compiler = webpack(webpackConfig)
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: '/assets/'
}))
app.use(require('webpack-hot-middleware')(compiler))
}
app.use(express.static(path.join(__dirname, '..', 'public')))
// Passport strategies
app.use(passport.initialize())
const BearerStrategy = require('passport-http-bearer').Strategy
const AnonymousStrategy = require('passport-anonymous').Strategy
const LocalStrategy = require('passport-local').Strategy
passport.use('bearer', new BearerStrategy(
function (token, done) {
jwt.verify(token, config.secret, function (err, decoded) {
if (!err) {
return done(null, decoded.id, {username: decoded.username, id: decoded.id})
} else {
return done(null)
}
})
}
))
passport.use('anonymous', new AnonymousStrategy())
passport.use('local', new LocalStrategy(function (username, password, done) {
console.log('User finding:', username)
User.findByUsername(username).then(function (user) {
console.log('User found:', user)
if (!user) {
return done(null, false, { message: 'Wrong username.' })
}
if (!user.validPassword(password)) {
console.log('Invalid password for user:', username)
return done(null, false, { message: 'Wrong password.' })
}
console.log('User returned', user)
return done(null, user, {id: user.id})
}).catch(function (err) {
console.log('User not found', err)
if (err) { return done(err) }
})
}))
// Main API
app.use('/api', api)
// Serve the index page for front end
app.use('/manage', index)
app.use('/', index)
// catch 404 and forward to error handler
app.use(function (req, res, next) {
const err = new Error('Not Found')
err.status = 404
next(err)
})
app.use(function (err, req, res, next) {
// development error handler, will print stacktrace
if (app.get('env') === 'dev' || app.get('env') === 'test') {
console.log(err)
console.log(err.stack)
}
if (err.name === 'ConflictError') {
return res.status(409).json({ message: err.message })
} else if (err.name === 'AuthorizationError') {
res.status(403).json({ message: err.message })
} else {
res.status(err.status || 500).json({ message: err.message })
}
})
module.exports = app
*
!.gitignore
'use strict'
class AuthorizationError extends Error {
constructor (message, status) {
super(message)
Error.captureStackTrace(this, 'AuthorizationError')
this.name = 'AuthorizationError'
this.message = message
this.status = status || 401
}
}
module.exports = AuthorizationError
'use strict'
class ConflictError extends Error {
constructor (message, status) {
super(message)
Error.captureStackTrace(this, 'ConflictError')
this.name = 'ConflictError'
this.message = message
this.status = status || 409
}
}
module.exports = ConflictError
'use strict'
class NotFoundError extends Error {
constructor (message, status) {
super(message)
Error.captureStackTrace(this, 'NotFoundError')
this.name = 'NotFoundError'
this.message = message || 'Not found'
this.status = status || 404
}
}
module.exports = NotFoundError
'use strict'
const User = require('./User')
const Fragment = require('./Fragment')
const Collection = require('./Collection')
const AuthorizationError = require('../errors/AuthorizationError')
class Authorize {
constructor (properties) {
}
// Check if permissions exist in a global scope
// e.g. admin can delete all /api/users
static _global (userId, resource, action) {
console.log('_global', userId, resource, action)
return acl.isAllowed(userId, resource, action).then(function (res) {
if (res) {
console.log(userId, 'is allowed to', action, resource)
return true
} else {
throw new AuthorizationError(userId +
' is not allowed to ' +
action + ' ' + resource
)
}
})
}
// Check if permissions exist in a local scope, for a single
// thing, e.g. contributor can edit fragment
static _local (userId, thing, model, id, action) {
console.log('_local', userId, thing, model, id, action)
var resource
var Model
switch (model) {
case 'collection':
Model = Collection
break
case 'collection/fragments':
Model = Fragment
break
case 'users':
Model = User
break
}
return Model.find(id).then(function (thing) {
resource = thing
// A user can delete or update owned objects and itself
if ((thing.owner && userId === thing.owner) ||
(thing.type === 'user' && thing.id === userId)
) {
return true
} else {
return false
}
}).then(function (owner) {
if (owner) {
return resource
} else {
if (model === 'collection/fragments') {
return this._global(userId, '/api/collection/fragments', action)
} else {
return this._global(userId, '/api/' + model, action)
}
}
}.bind(this)).then(function (res) {
if (res) {
return resource
} else {
throw new AuthorizationError(userId +
' is not allowed to ' +
action + ' ' + thing
)
}
})
}
// Checks for permissions and resolves with the thing asked about,
// if it makes sense (for reading, updating, deleting single objects)
static it (userId, thing, action) {
if (!userId) {
return Promise.reject(new AuthorizationError())
}
console.log('Finding out if', userId, 'can', action, thing)
if (action === 'delete' || action === 'update' || action === 'read') {
var splitted = thing.split('/')
var id = splitted[3] // e.g. /api/users/1
if (id === 'fragments') { // e.g. /api/collection/fragments/1
var model = 'collection/fragments'
id = splitted[4]
} else {
model = splitted[2]
}
if (!id && model) {
return this._global(userId, '/api/' + model, action)
} else {
return this._local(userId, thing, model, id, action)
}
} else {
return this._global(userId, thing, action)
}
}
}
module.exports = Authorize
'use strict'
const _ = require('lodash')
const Model = require('./Model')
const Fragment = require('./Fragment')
const User = require('./User')
class Collection extends Model {
constructor (properties) {
super(properties)
this.type = 'collection'
this.title = properties.title
this.id = 1
}
// Gets fragments in a collection, supports filtering by properties
// e.g. collection.getFragments({filter: {published: true, owner: req.user}})
getFragments (options) {
options = options || {}
if (!this.fragments) { return [] }
var fragments = this.fragments.map(function (id) {
return Fragment.find(id)
})
var filteredFragments
return Promise.all(fragments).then(function (fragments) {
return fragments.filter(function (fragment) {
if (options.filter) {
fragment = _.some(_.map(options.filter, function (value, key) {
return fragment[key] === value
}))
}
return fragment
})
}).then(function (fragments) {
filteredFragments = fragments
return Promise.all(fragments.map(function (fragment) {
return User.find(fragment.owner)
}))
}).then(function (owners) {
return owners.reduce(function (map, owner) {
map[owner.id] = owner.username
return map
}, {})
}).then(function (ownersById) {
return filteredFragments.map(function (fragment) {
fragment.owner = ownersById[fragment.owner]
return fragment
})
})
}
addFragment (fragment) {
if (this.fragments) {
this.fragments.push(fragment)
} else {
this.fragments = [fragment]
}
}
}
Collection.type = 'collection'
module.exports = Collection
'use strict'
const Model = require('./Model')
class Fragment extends Model {
constructor (properties) {
super(properties)
this.type = 'fragment'
this.title = properties.title
}
}
Fragment.type = 'fragment'
module.exports = Fragment
'use strict'
const schema = require('./schema')
const uuid = require('node-uuid')
const NotFoundError = require('../errors/NotFoundError')
schema()
class Model {
constructor (properties) {
schema()
this.id = Model.uuid()
Object.assign(this, properties)
}
save () {
console.log('Saving', this, this.id)
return this.constructor.find(this.id).then(function (result) {
console.log('Found an existing version, this is an update of:', result)
return result.rev
}).then(function (rev) {
this.rev = rev
return this._put()
}.bind(this)).catch(function (error) {
if (error && error.status === 404) {
console.log('No existing object found, creating a new one:', error)
return this._put()
} else {
throw error
}
}.bind(this))
}
_put () {
return db.rel.save(this.constructor.type, this).then(function (response) {
console.log('Actually _put', this)
return this
}.bind(this))
}
delete () {
return this.constructor.find(this.id).then(function (object) {
return db.rel.del(this.type, object)
}.bind(this)).then(function () {
console.log('Deleted', this.type, this.id)
return this
}.bind(this))
}
updateProperties (properties) {
// TODO: Owner can not be changed
delete properties.owner
console.log('Updating properties to', properties)
// TODO: Should we screen/filter more properties here?
Object.assign(this, properties)
return this
}
static uuid () {
return uuid.v4()
}
// Find all of a certain type e.g.
// User.all()
static all (options) {
options = options || {}
return db.rel.find(this.type)
.then(function (results) {
return results[this.type + 's']
}.bind(this)).catch(function (err) {
console.error(err)
})
}
// Find by id e.g.
// User.find('394')
static find (id, options) {
let plural = this.type + 's'
return db.rel.find(this.type, id).then(function (results) {
if (results[plural].length === 0) {
throw new NotFoundError()
} else {
return new this(results[plural][0])
}
}.bind(this)).catch(function (err) {
if (err.name === 'NotFoundError') {
console.log('Object not found', err)
}
throw err
})
}
static findByField (field, value) {
console.log('Finding', field, value)
field = 'data.' + field
let type = 'data.type'
return db.createIndex({
index: {
fields: [field, type]
}
}).then(function (result) {
var selector = {selector: {}}
selector.selector[type] = this.type
selector.selector[field] = value
return db.find(selector)
}.bind(this)).then(results => {
if (results.docs.length === 0) {
throw new NotFoundError()
} else {
return results.docs.map(result => {
let id = db.rel.parseDocID(result._id).id
let foundObject = result.data
foundObject.id = id
foundObject.rev = result._rev
return new this(foundObject)
})
}
}).catch(function (err) {
if (err.name !== 'NotFoundError') {
console.error('Error', err)
throw err
} else {
throw err
}
})
}
}
module.exports = Model
'use strict'
class Role {
constructor (properties) {
this.type = 'role'
this.name = properties.name
this.resources = properties.resources
this.permissions = properties.permissions || '*'
}
save () {
return new Promise(function (resolve, reject) {
acl.allow(this.name, this.resources, this.permissions, function (err) {
if (err) {
console.error(err)
reject(err)
} else {
console.log('Saving', this.type, this.name)
resolve(this)
}
}.bind(this))
}.bind(this))
}
static userRoles (userId) {
return acl.userRoles(userId)
}
static addUserRoles (userId, role, User) {
return acl.addUserRoles(userId, role).then(function () {
return this.syncRoles(userId, User)
}.bind(this)).catch(function (err) {
console.error(err)
throw err
})
}
static syncRoles (userId, User) {
// Roles is a property on the User model that is synced with the ACL system
let roles
return this.userRoles(userId).then(function (existingRoles) {
roles = existingRoles
return User.find(userId)
}).then(function (user) {
user.roles = roles
return user.save()
})
}
static removeUserRoles (userId, role, User) {
return acl.removeUserRoles(userId, role).then(function () {
return this.syncRoles(userId, User)
}.bind(this)).catch(function (err) {
console.error(err)
throw err
})
}
static findByName (name) {
return acl.whatResources(name, function (resources) {
return new this({
name: name,
resources: resources
})
})
}
}
module.exports = Role
'use strict'
const Model = require('./Model')
const Role = require('./Role')
const ConflictError = require('../errors/ConflictError')
const bcrypt = require('bcryptjs')
const _ = require('lodash')
class User extends Model {
constructor (properties) {
super(properties)
// Hash and delete the password if it's set
if (properties.password) {
this.passwordHash = bcrypt.hashSync(properties.password, 1)
delete this.password
}
this.type = 'user'
this.roles = properties.roles || []
this.email = properties.email
this.username = properties.username
}
updateProperties (properties) {
// Roles are updated separately in an async manner
var roles = properties.roles
delete properties['roles']
super.updateProperties(properties)
if (roles) {
return this.setRoles(roles).then(function () {
this.roles = roles
return this
}.bind(this))
} else {
return this
}
}
validPassword (password) {
return bcrypt.compareSync(password, this.passwordHash)
}
addRole (role) {
return Role.addUserRoles(this.id, role, User).then(function () {
return this
}.bind(this))
}
removeRole (role) {
return Role.removeUserRoles(this.id, role, User).then(function () {
return this
}.bind(this))
}
// e.g. user.setRoles(['admin', 'contributor'])
setRoles (newRoles) {
var rolesToAdd = _.difference(newRoles, this.roles)
var rolesToRemove = _.difference(this.roles, newRoles)
var promises = rolesToAdd.map(function (role) {
return this.addRole(role)
}.bind(this))
promises.concat(rolesToRemove.map(function (role) {
return this.removeRole(role)
}.bind(this)))
return Promise.all(promises).then(function () {
return newRoles
}).catch(function (err) {
throw err
})
}
isUniq () {
return User.findByEmail(this.email).then(function (user) {
throw new ConflictError('User exists')
}).catch(function (err) {
if (err.name === 'NotFoundError') {
return User.findByUsername(this.username)
} else {
throw err
}
}.bind(this)).then(function (user) {
throw new ConflictError('User exists')
}).catch(function (err) {
if (err.name === 'NotFoundError') {
return true
} else {
throw err
}
})
}
static findByEmail (email) {
return this.findByField('email', email).then(function (users) {
return users[0]
})
}
static findByUsername (username) {
return this.findByField('username', username).then(function (users) {
return users[0]
})
}
}
User.type = 'user'
module.exports = User
'use strict'
var PouchDB = require('pouchdb')
PouchDB.plugin(require('pouchdb-find'))
PouchDB.plugin(require('relational-pouch'))
PouchDB.plugin(require('pouchdb-upsert'))
const AclPouchDb = require('node_acl_pouchdb')
global.db = new PouchDB('./api/db/' + process.env.NODE_ENV)
global.acl = new AclPouchDb(new AclPouchDb.pouchdbBackend(db, 'acl'))
module.exports = function () {
if (!db.rel) {
return db.setSchema([
{
singular: 'collection',
plural: 'collections',
relations: {
fragments: {hasMany: 'fragment'},
owner: {belongsTo: 'user'}
}
},
{
singular: 'fragment',
plural: 'fragments',
relations: {
collection: {belongsTo: 'collection'},
owner: {belongsTo: 'user'}
}
},
{
singular: 'user',
plural: 'users',
relations: {
collections: {hasMany: 'collection'},
fragments: {hasMany: 'fragment'}
}