P.BARRY 6 years ago
parent
commit
9d3376df70
31 changed files with 6629 additions and 0 deletions
  1. 26
      Dockerfile
  2. 7
      apidoc.js
  3. 54
      config.js
  4. 121
      index.js
  5. 23
      mocha.entry.js
  6. 4305
      package-lock.json
  7. 55
      package.json
  8. 8
      services/core/config/router.js
  9. 24
      services/core/config/router.spec.js
  10. 201
      services/media/router.js
  11. BIN
      services/media/rsc/default/404.jpg
  12. 70
      services/restapi/collections/__template.js
  13. 36
      services/restapi/collections/config/config.form.js
  14. 18
      services/restapi/collections/config/config.js
  15. 38
      services/restapi/collections/landingpages/landingpages.form.js
  16. 32
      services/restapi/collections/landingpages/landingpages.js
  17. 21
      services/restapi/collections/media/media.js
  18. 14
      services/restapi/collections/users-filters/users-filters.js
  19. 58
      services/restapi/collections/users/users.form.js
  20. 47
      services/restapi/collections/users/users.js
  21. 39
      services/restapi/collections/users/users.spec.js
  22. 734
      services/restapi/router.js
  23. 25
      services/restapi/router.spec.js
  24. 89
      services/session/auth.js
  25. 118
      services/session/router.js
  26. 171
      services/session/router.spec.js
  27. 107
      services/session/services.js
  28. 70
      services/session/services.spec.js
  29. 43
      tools/common.js
  30. 53
      tools/middleware-done.js
  31. 22
      tools/time-profiler.js

26
Dockerfile

@ -0,0 +1,26 @@
# build static files
FROM node:carbon-alpine as build-nubium-cloud
WORKDIR /app
COPY package*.json ./
RUN npm install
# final image
FROM node:carbon-alpine
## install bash
#RUN apk add bash docker
WORKDIR /app
RUN mkdir -p /data/nubium-artifex
# pkg not working in docker
COPY --from=build-nubium-cloud /app/ .
COPY . .
ENV PORT 3009
EXPOSE 3009
ENTRYPOINT ["node", "index.js"]

7
apidoc.js

@ -0,0 +1,7 @@
// ////////////////////////////////////////////////////////////////////////////
// Current Errors.
// ////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////
// Current Params.
// ////////////////////////////////////////////////////////////////////////////

54
config.js

@ -0,0 +1,54 @@
const path = require('path')
const CFG = {}
const CFG_DB_NAME = 'earth'
CFG.media = {
cache: path.resolve('/data/nubium-artifex/cache'),
documents: path.resolve('/data/nubium-artifex/documents'),
'404': path.resolve(__dirname, './services/media/rsc/default/404.jpg')
}
// console.log(CFG.media)
CFG.server = {
port: process.env.PORT || 3009
}
CFG.mongo = {
url: `mongodb://localhost:27017/${CFG_DB_NAME}`,
db: CFG_DB_NAME
}
CFG.jwt = {
enable: true,
jwtAuth: {
secret: '@@Earth@@Hello0WorlD!!!!',
passSalt: '@@Earth@@Hello0WorlDForP@55W0RD!!!!',
expiresIn: '15d'
},
jwtAccess: {
secret: '@@Earth@@Hello0WorlD!!!!',
expiresIn: '24h'
},
sid: 1
}
// ////////////////////////////////////////////////////////////////////////////
// UP FROM ENV
if (process.env.ENV === 'dev') {
CFG.mongo.url = 'mongodb://localhost:27017'
CFG.jwt.enable = false
}
if (process.env.ENV === 'prod') {
CFG.mongo.url = 'mongodb://rw:pxcom@airpmp.aero:27017/admin'
}
if (process.env.ENV === 'preprod') {
CFG.mongo.url = 'mongodb://localhost:27017'
}
console.log(CFG.mongo.url)
module.exports = CFG

121
index.js

@ -0,0 +1,121 @@
const express = require('express')
const app = express()
const morgan = require('morgan')
const bodyParser = require('body-parser')
const mongoClient = require('mongodb').MongoClient
const fs = require('fs-extra')
const session = require('./services/session/router')
const restapi = require('./services/restapi/router')
const media = require('./services/media/router')
const coreCfg = require('./services/core/config/router')
const CFG = require('./config')
const PKG = require('./package')
const auth = require('./services/session/auth')
const CTX = {
cfg: CFG,
logEnabled: true,
dbHandler: null,
db: null,
tools: require('./tools/common'),
models: {}
}
let server = {
handler: null,
interval: null,
onReady: function () {}
}
app.use(morgan('tiny', {
skip: function (req, res) { return !CTX.logEnabled }
}))
app.use(bodyParser.json())
app.use(require('./tools/middleware-done'))
mongoClient.connect(CFG.mongo.url, { useNewUrlParser: true }, function (pErr, pDbHandler) {
if (pErr) {
if (CTX.logEnabled) {
console.error(`[!] Failed to connect DB: ${CFG.mongo.url}`)
console.error(pErr)
}
process.exit(1)
}
// //////////////////////////////////////////////////////////////////////////
// Init folder
fs.ensureDirSync(CFG.media.documents)
fs.ensureDirSync(CFG.media.cache)
// //////////////////////////////////////////////////////////////////////////
// DB HANDLING
if (CTX.logEnabled) console.log(`[-] Connected to mongoDB: ${CFG.mongo.url}`)
CTX.dbHandler = pDbHandler
CTX.db = CTX.dbHandler.db(CFG.mongo.db)
addUserIfRequired()
// //////////////////////////////////////////////////////////////////////////
// ROUTING
app.use('/session', session(CTX))
app.use('/doc', express.static('../doc/api'))
app.get('/', (req, res) => {
res.send(`Hello World from Earth server - v${PKG.version} - build ${CTX.tools.git.currentRevision}`)
})
// ALL API after auth call required authentification
app.use(auth(CTX))
app.use('/api', restapi(CTX))
app.use('/media', media(CTX))
app.use('/core/config', coreCfg(CTX))
// //////////////////////////////////////////////////////////////////////////
// LISTENING
server.handler = app.listen(CFG.server.port, () => {
if (CTX.logEnabled) console.log(`[-] Current build: ${CTX.tools.git.currentRevision}`)
if (CTX.logEnabled) console.log(`[-] Earth server listening on: ${CFG.server.port}`)
server.onReady()
})
})
app.stop = function () {
server.interval = setTimeout(function () {
server.handler.close()
CTX.db.handler.close()
}, 500)
}
app.setOnReady = function (done) {
clearTimeout(server.interval)
server.onReady = done
if (server.handler) {
server.onReady()
}
}
app.setLog = function (pFlag) {
CTX.logEnabled = pFlag
}
module.exports = app
function addUserIfRequired () {
let dbUsers = CTX.db.collection('users')
dbUsers.find({}).count(function (pErr, pCount) {
if (!pCount) {
let user = {
username: 'admin',
email: 'admin@earth.com',
details: {
firstname: 'Admin', lastname: 'Admin', company: 'Earth', corporateTitle: 'BigBoss', language: 'en'
},
rights: {
role: 'admin'
},
multipass: {
hash: 'admin'
}
}
dbUsers.insert(user, function () {})
}
})
}

23
mocha.entry.js

@ -0,0 +1,23 @@
/* eslint-env mocha */
const server = require('./index')
server.setLog(false)
global.login = {user: 'airpmp', pass: 'admin'}
before(function (done) {
global.server = server
global.jwtAccess = ''
this.timeout(10000)
server.setOnReady(function () {
done()
})
})
require('./services/session/router.spec')
require('./services/restapi/router.spec')
require('./services/core/config/router.spec')
after(function () {
server.stop()
})

4305
package-lock.json
File diff suppressed because it is too large
View File

55
package.json

@ -0,0 +1,55 @@
{
"name": "ground",
"version": "1.0.0",
"description": "Services and more for Earth ground",
"main": "index.js",
"scripts": {
"test": "mocha mocha.entry.js",
"test:all": "mocha './{,!(node_modules)/**/}*.spec.js'",
"lint": "standard",
"doc:api": "apidoc -i . -o ../doc/api -f services/ -f ./apidoc.js",
"doc:pdf": "pxdoc ../doc/PXEN302-0014_DSN_AIR_PMP_SERVER.md",
"doc": "npm run doc:api && npm run doc:pdf",
"dev": "ENV=dev nodemon"
},
"repository": {
"type": "git",
"url": "ssh://gitolite@tuleap.pxcom.aero/airpmp/airpmp-server.git"
},
"keywords": [],
"apidoc": {
"title": "Earth - Cloud",
"url": "http://earth.codeisalie.fr"
},
"author": "P.BARRY",
"license": "ISC",
"dependencies": {
"ajv": "^6.5.2",
"async": "^2.6.1",
"axios": "^0.18.0",
"body-parser": "^1.18.3",
"camelcase": "^5.0.0",
"colors": "^1.3.2",
"exceljs": "^1.6.0",
"express": "^4.16.3",
"faker": "^4.1.0",
"fs-extra": "^7.0.0",
"json-schema-faker": "^0.5.0-rc15",
"jsonwebtoken": "^8.3.0",
"moment": "^2.22.2",
"mongodb": "^3.1.1",
"morgan": "^1.9.0",
"multer": "^1.4.1",
"request-ip": "^2.1.1",
"sharp": "^0.22.1",
"slug": "^0.9.1",
"slugify": "^1.3.1"
},
"devDependencies": {
"apidoc": "^0.17.6",
"chai": "^4.1.2",
"chai-http": "^4.0.0",
"mocha": "^5.2.0",
"standard": "^11.0.1"
}
}

8
services/core/config/router.js

@ -0,0 +1,8 @@
const express = require('express')
const router = express.Router()
const CFG = require('../../../config')
module.exports = function (pCtx) {
return router
}

24
services/core/config/router.spec.js

@ -0,0 +1,24 @@
/* eslint-env mocha */
/* eslint no-unused-expressions: 0 */
const chai = require('chai')
const chaiHttp = require('chai-http')
const expect = chai.expect
chai.use(chaiHttp)
describe('Core - Config', function () {
describe('# Foo', function () {
it.skip('GET /core/config/foo', function (done) {
chai.request(global.server)
.get('/core/config/foo')
.set('token', global.jwtAccess || '')
.end((err, res) => {
// console.log(res.body)
expect(err).to.be.null
expect(res).to.have.status(200)
done()
})
})
})
})

201
services/media/router.js

@ -0,0 +1,201 @@
const express = require('express')
const router = express.Router()
const async = require('async')
const path = require('path')
const multer = require('multer')
const fs = require('fs-extra')
const upload = multer({ dest: 'uploads' })
const sharp = require('sharp')
const TOL = require('../../tools/common')
const CFG = require('../../config')
var CTX = null
module.exports = function (pCtx) {
CTX = pCtx
router.post('/upload', upload.single('media'), handleUploadedMedia)
router.get('/get/:id', function (req, res) {
let mediaId = req.params.id
CTX.restapi.media.__services.readOne(mediaId, {projection: {path: 1}}, function (pErr, pResult) {
if (pErr) {
return res.status(404).send('Media:Media:NotFound')
}
let mediaPath = path.join(CFG.media.documents, pResult.data.path)
fs.access(mediaPath, fs.constants.F_OK, function (pErr) {
if (pErr) {
return res.status(404).send('Media:File:NotFound')
}
res.sendFile(mediaPath)
})
})
})
router.get('/preview/:id', function (req, res) {
let mediaId = req.params.id
CTX.restapi.media.__services.readOne(mediaId, {projection: {mimetype: 1, path: 1}}, function (pErr, pResult) {
if (pErr) {
return res.sendFile(CFG.media['404'])
}
let media = pResult.data
let mimetypes = media.mimetype.split('/')
let type = mimetypes[0]
let subtype = mimetypes[1]
switch (type) {
case 'image':
let mediaPath = path.join(CFG.media.documents, media.path)
getPreviewFromImage(res, mediaPath, req.query)
break
default:
res.sendFile(CFG.media['404'])
}
})
})
return router
}
function getPreviewFromImage (res, pPath, pQuery, pMedia) {
let query = pQuery
query.width = parseInt(query.width) || null
query.height = parseInt(query.height) || null
query.fit = query.fit || 'cover'
if (!query.width && !query.height) {
query.width = 1024
}
let outName = TOL.createHash(JSON.stringify(query), 'sha1')
let outPath = path.join(CFG.media.cache, generatePathFromMediaId(outName))
let outFull = path.join(outPath, `${outName}.jpg`)
let tasks = []
tasks.push(function (pCb) {
fs.ensureDir(outPath, pErr => pCb(pErr))
})
tasks.push(function (pCb) {
sharp(pPath)
.resize(query.width, query.height, {fit: query.fit})
.toFile(outFull, function (pErr) {
pCb(pErr)
})
})
async.waterfall(tasks, function (pErr) {
if (pErr) {
console.log(pErr)
res.sendFile(CFG.media['404'])
} else {
res.sendFile(outFull)
}
})
}
function handleUploadedMedia (req, res) {
let tasks = []
let file = req.file
let fileKey = {
coll: '__default',
id: '',
path: ''
}
let ext = getExtensionFromMimetype(file.mimetype)
let currentDate = new Date()
if (req.body.docColl && req.body.docId && req.body.docPath) {
fileKey = {
coll: req.body.docColl,
id: req.body.docId,
path: req.body.docPath
}
}
let mediaId = TOL.createHash(JSON.stringify(fileKey.coll === '__default' ? file : fileKey), 'sha1')
let outPath = path.join(CFG.media.documents, generatePathFromMediaId(mediaId))
let outName = `media.${ext}`
let outFull = path.join(outPath, outName)
let isAlreadyExisting = false
file.createdAt = currentDate
file.fileKey = fileKey
tasks.push(function (pCb) {
fs.ensureDir(outPath, pErr => pCb(pErr))
})
tasks.push(function (pCb) {
fs.move(file.path, outFull, { overwrite: true }, pErr => pCb(pErr))
})
tasks.push(function (pCb) {
fs.writeFile(path.join(outPath, 'info'), JSON.stringify(file, null, 2), 'utf8', pErr => pCb(pErr))
})
tasks.push(function (pCb) {
CTX.restapi.media.__services.readOne(mediaId, {projection: {_id: 1}}, function (pErr, pResult) {
if (pResult && pResult.ok) {
isAlreadyExisting = true
}
pCb()
})
})
tasks.push(function (pCb) {
let doc = {
_id: mediaId,
name: outName,
originalName: file.originalname,
extension: ext,
path: path.join(generatePathFromMediaId(mediaId), outName),
fileKey: fileKey,
mimetype: file.mimetype,
size: file.size
}
if (isAlreadyExisting) {
CTX.restapi.media.__services.update(mediaId, doc, req.user, 'media', function (pErr, pResult) {
pCb(pErr)
})
} else {
CTX.restapi.media.__services.create(doc, req.user, function (pErr, pResult) {
pCb(pErr)
})
}
})
async.waterfall(tasks, function (pErr) {
res.done(pErr, {
ok: pErr ? 0 : 1,
mediaId: mediaId
})
})
}
function generatePathFromMediaId (pMediaId) {
let outSubf = pMediaId.slice(0, 4)
return path.join(outSubf, pMediaId)
}
function getExtensionFromMimetype (pMimetype) {
let ext = 'unknown'
let mimetypes = pMimetype.split('/')
let type = mimetypes[0]
let subtype = mimetypes[1]
switch (type) {
case 'audio':
case 'video':
case 'image':
ext = subtype
break
case 'application/pdf':
ext = 'pdf'
break
default:
}
return ext
}

BIN
services/media/rsc/default/404.jpg

Before After
Width: 800  |  Height: 450  |  Size: 12 KiB

70
services/restapi/collections/__template.js

@ -0,0 +1,70 @@
module.exports = function (pName, pLabel, pIcon, pDesc) {
return {
name: pName,
label: pLabel || pName,
icon: pIcon || 'mdi-hexagon',
description: pDesc || '---',
/** MongoDB indexes - format: {keys:, options:} */
indexes: [],
/** JSON Schema */
schema: {},
hooks: {
read: {
before: (pCtx, next) => {
next()
},
after: (pCtx, docs, next) => {
next()
}
},
readOne: {
before: (pCtx, id, next) => {
next()
},
after: (pCtx, doc, next) => {
next()
}
},
update: {
before: (pCtx, id, doc, origin, next) => {
next()
},
after: (pCtx, doc, origin, next) => {
next()
}
},
create: {
before: (pCtx, doc, next) => {
next()
},
after: (pCtx, doc, next) => {
next()
}
},
delete: {
before: (pCtx, id, next) => {
next()
},
after: (pCtx, ok, next) => {
next()
}
}
},
tools: {},
/** Db handler of collection */
__db: null,
__services: {
read: function (pArgs, pCallback) { pCallback() },
readOne: function (pId, pArgs, pCallback) { pCallback() },
create: function (pDoc, pUser, pCallback) { pCallback() },
update: function (pId, pDoc, pUser, pCallback) { pCallback() },
delete: function (pId, pUser, pCallback) { pCallback() }
},
__internal: {
router: require('express').Router(),
addRoute: function (pMethod, pUrl, pFunc) {
this.router[pMethod.toLowerCase()](pUrl, pFunc)
}
}
}
}

36
services/restapi/collections/config/config.form.js

@ -0,0 +1,36 @@
module.exports = function (pCtx, coll) {
coll.formCreate = [
{
size: '',
list: [
{ field: 'name', label: 'Nom du champ', type: 'text', rules: {required: 'Nom manquant'} },
{ field: '_id', label: 'Clé', type: 'text', rules: {required: 'Clé manquante'} }
]
}
]
coll.form = [
{
label: 'General',
field: '',
form: [
{
size: '',
list: [
{ field: 'name', label: 'Nom', type: 'text' },
{ field: '_id', label: 'Clé', type: 'text', disabled: true },
{ field: 'value', label: 'Value', type: 'text' }
]
}
]
}
]
coll.listingHeader = [
{ text: 'Nom', align: 'left', value: 'name' },
{ text: 'Clé', align: 'center', value: '_id' },
{ text: 'Valeur', align: 'center', value: 'value' }
]
}

18
services/restapi/collections/config/config.js

@ -0,0 +1,18 @@
const Template = require('../__template')
const coll = new Template('config', 'Configuration', 'mdi-bookmark', 'Configuration du nubium artifex')
coll.customId = true
coll.schema = {
type: 'object',
properties: {
_id: { type: 'string' },
name: { type: 'string' },
value: {}
}
}
module.exports = function (pCtx) {
require('./config.form')(pCtx, coll)
return coll
}

38
services/restapi/collections/landingpages/landingpages.form.js

@ -0,0 +1,38 @@
module.exports = function (pCtx, coll) {
coll.formCreate = [
{
size: '',
list: [
{ field: 'name', label: 'Nom', type: 'text', rules: {required: 'Nom manquant'} }
]
}
]
coll.form = [
{
label: 'General',
field: '',
form: [
{
size: '',
list: [
{ field: 'name', label: 'Nom', type: 'text' },
{ field: 'description', label: 'Description', type: 'textarea' }
]
},
{
size: '',
list: [
{ field: 'preview', label: 'Preview', type: 'media' }
]
}
]
}
]
coll.listingHeader = [
{ text: 'Nom', align: 'left', value: 'name' }
]
}

32
services/restapi/collections/landingpages/landingpages.js

@ -0,0 +1,32 @@
const Template = require('../__template')
const coll = new Template('landingpages', 'Landing Pages', 'mdi-home', 'Pages d\'accueil')
coll.indexes.push({keys: {name: 1}, options: {unique: true}})
coll.schema = {
type: 'object',
additionalProperties: false,
required: ['name'],
properties: {
name: {
type: 'string'
},
description: {
type: 'string'
},
preview: {
type: 'string'
},
page: {
type: 'array',
items: {
type: 'array'
}
}
}
}
module.exports = function (pCtx) {
require('./landingpages.form')(pCtx, coll)
return coll
}

21
services/restapi/collections/media/media.js

@ -0,0 +1,21 @@
const Template = require('../__template')
const coll = new Template('media')
coll.customId = true
coll.schema = {
type: 'object',
properties: {
name: { type: 'string' },
originalName: { type: 'string' },
mimetype: { type: 'string' },
extension: { type: 'string' },
path: { type: 'string' },
fileKey: { type: 'object' },
size: { type: 'number' }
}
}
module.exports = function (pCtx) {
return coll
}

14
services/restapi/collections/users-filters/users-filters.js

@ -0,0 +1,14 @@
const Template = require('../__template')
const coll = new Template('users-filters')
coll.schema = {
type: 'object',
properties: {
name: { type: 'string' },
rules: { type: 'object' }
}
}
module.exports = function (pCtx) {
return coll
}

58
services/restapi/collections/users/users.form.js

@ -0,0 +1,58 @@
module.exports = function (pCtx, coll) {
coll.formCreate = [
{
size: '',
list: [
{
field: 'username',
label: 'Nom d\'utilisateur',
type: 'text',
rules: {required: 'Nom manquant', regex: '[a-zA-Z0-9_-];i;Le nom doit respecter le format [a-zA-Z0-9]'}
},
{ field: 'email', label: 'Courriel', type: 'text', rules: {required: 'Courriel manquant'} }
]
}
]
coll.form = [
{
label: 'General',
field: '',
form: [
{
size: '',
list: [
{ field: 'username', label: 'Nom d\'utilisateur', type: 'text' },
{ field: 'email', label: 'Courriel', type: 'text' }
]
},
{
size: '',
list: [
{ field: 'avatar', label: 'Avatar', type: 'media' }
]
}
]
},
{
label: 'Details',
field: 'details',
form: [
{
size: '',
list: [
{ field: 'firstname', label: 'Prénom', type: 'text' },
{ field: 'lastname', label: 'Nom', type: 'text' }
]
}
]
}
]
coll.listingHeader = [
{ text: 'Username', align: 'left', value: 'username' },
{ text: 'Courriel', align: 'center', value: 'email' }
]
}

47
services/restapi/collections/users/users.js

@ -0,0 +1,47 @@
const Template = require('../__template')
const users = new Template('users')
users.indexes.push({keys: {email: 1}, options: {unique: true}})
users.schema = {
type: 'object',
additionalProperties: false,
required: ['username', 'email'],
properties: {
username: {
type: 'string'
},
email: {
type: 'string',
format: 'email'
},
details: {
type: 'object',
properties: {
firstname: { type: 'string' },
lastname: { type: 'string' },
language: { type: 'string' },
company: { type: 'string' },
corporateTitle: { type: 'string' }
}
},
multipass: {
type: 'object',
properties: {
hash: {
type: 'string'
}
}
}
}
}
users.hooks.readOne.after = function (ctx, doc, next) {
doc.name = `${doc.username}`
next()
}
module.exports = function (pCtx) {
require('./users.form')(pCtx, users)
return users
}

39
services/restapi/collections/users/users.spec.js

@ -0,0 +1,39 @@
/* eslint-env mocha */
/* eslint no-unused-expressions: 0 */
const chai = require('chai')
const chaiHttp = require('chai-http')
const expect = chai.expect
chai.use(chaiHttp)
describe('# Users', function () {
it('GET /api/users', function (done) {
chai.request(global.server)
.get('/api/users')
.set('token', global.jwtAccess)
.end((err, res) => {
// console.log(res.status)
expect(err).to.be.null
expect(res).to.have.status(200)
expect(res.body).to.have.property('data')
expect(res.body.data).to.have.lengthOf.at.least(1)
done()
})
})
})
describe('# Users - Filters', function () {
it('GET /api/users-filters', function (done) {
chai.request(global.server)
.get('/api/users-filters')
.set('token', global.jwtAccess)
.end((err, res) => {
// console.log(res.status)
expect(err).to.be.null
expect(res).to.have.status(200)
expect(res.body).to.have.property('data')
done()
})
})
})

734
services/restapi/router.js

@ -0,0 +1,734 @@
const express = require('express')
const router = express.Router()
const path = require('path')
const fs = require('fs')
const ObjectId = require('mongodb').ObjectId
const async = require('async')
const Ajv = require('ajv')
const jsf = require('json-schema-faker')
// const CFG = require('../../config')
const TAG = 'REST'
const REST_CTX = {
models: [],
api: [
{method: 'GET', url: '/:collection', desc: ''},
{method: 'POST', url: '/:collection', desc: ''},
{method: 'PUT', url: '/:collection/:id', desc: ''},
{method: 'DELETE', url: '/:collection', desc: ''},
{method: 'GET', url: '/:collection/private/json-schema', desc: ''},
{method: 'GET', url: '/:collection/private/json-schema/fake', desc: ''}
]
}
jsf.extend('faker', function () {
return require('faker')
})
// router.use((req, res, next) => {
// console.log('API isAuth', req.isAuth)
// next()
// })
function initCollections (pCtx) {
pCtx.restapi = {}
fs.readdir(path.join(__dirname, './collections'), (pErr, pColls) => {
pColls.forEach((pColl) => {
if (/^__/.test(pColl)) return
let coll = require(path.join(__dirname, './collections', pColl, pColl))(pCtx)
if (pCtx.logEnabled) console.log(`[-] ${TAG} | Loading collection: ${coll.name}`)
// INIT INDEXES
coll.indexes = coll.indexes || []
coll.indexes.forEach((pIndex) => {
pCtx.db.collection(coll.name).createIndex(pIndex.keys, pIndex.options)
})
pCtx.restapi[coll.name] = coll
initRestApi(pCtx, coll)
REST_CTX.models.push(coll.name)
})
})
router.get('/__intern/collections', function (req, res, next) {
let all = []
for (let coll in pCtx.restapi) {
all.push({
coll: coll,
label: pCtx.restapi[coll].label,
icon: pCtx.restapi[coll].icon,
description: pCtx.restapi[coll].description
})
}
res.done(null, all.sort((a, b) => a.label.localeCompare(b.label)))
})
router.get('/', function (req, res, next) {
res.done(null, REST_CTX)
})
}
function initRestApi (pCtx, pColl) {
let collHandler = pCtx.db.collection(pColl.name)
let ajv = new Ajv({removeAdditional: true, allErrors: true})
let collValidator = ajv.compile(pColl.schema)
if (collValidator.error) {
console.error(collValidator)
process.exit(1)
}
router.use(function (req, res, next) {
generateFilterObject(req)
next()
})
pColl.__services = {
read: null,
readOne: null,
create: null,
update: null,
delete: null
}
pColl.__db = collHandler
pColl.__addMetadata = function (doc, user, req) {
doc.__metadata = {
owner: user ? user._id : null,
updatedBy: null,
createdAt: new Date(),
updatedAt: null,
deletedAt: null
}
}
pColl.__updateMetadata = function (doc, user) {
delete doc.__metadata
doc['__metadata.updatedAt'] = new Date()
if (user) doc['__metadata.updatedBy'] = user ? user._id : null
}
pColl.__generateFilterObject = generateFilterObject
pColl.__buildRestOp = buildRestOpObject
// Add new routes
if (pColl.__internal.router) router.use('/' + pColl.name + '/methods/', pColl.__internal.router)
/**
* @api {get} /api/:collection Get all
* @apiName get:/api/:collection
* @apiVersion 0.0.1
* @apiGroup Restapi
* @apiDescription Get all documents from one collection
*
* @apiParam {String} collection Required collection
*
* @apiSuccess {Object} res Response.
* @apiSuccess {Object} res.ok 1 if success.
* @apiSuccess {Object} res.data List of documents.
* @apiSuccess {Object} res.limit Results limitation.
* @apiSuccess {Object} res.skip Skipped results.
* @apiSuccess {Object} res.count Total results available on server.
* @apiSuccess {Object} res.links Links to navigate into documents.
*
* @apiUse SessionError
*/
router.get(`/${pColl.name}`, (req, res) => {
pColl.__services.read(req.restOp, res.done)
})
pColl.__services.read = function (pArgs, pCallback) {
let restOp = pArgs || buildRestOpObject()
// Find operator
let findOp = { $and: [] }
findOp.$and.push(restOp.filter)
// Projection operator
let projOp = Object.keys(restOp.projection).length ? restOp.projection : {}
// Find all matching elements
collHandler.find(findOp).count(function (pErr, pCount) {
collHandler
// Filters elements
.find(findOp)
// Project fields
.project(projOp)
// Sort result
.sort(restOp.sort)
// Limit the number
.limit(restOp.limit)
// Skip
.skip(restOp.skip)
// Get results
.toArray(function (pErr, pResults) {
if (pErr) return pCallback(pErr)
pCallback(null, {
ok: 1,
label: pCtx.restapi[pColl.name].label,
icon: pCtx.restapi[pColl.name].icon,
description: pCtx.restapi[pColl.name].description,
data: pResults,
limit: restOp.limit,
count: pCount,
skip: restOp.skip,
links: {
base: '',
next: `/api/${pColl.name}?limit=${restOp.limit}&skip=${restOp.skip + restOp.limit}`,
prev: `/api/${pColl.name}?limit=${restOp.limit}&skip=${Math.max(0, restOp.skip - restOp.limit)}`
},
sort: restOp.sort
})
})
})
}
/**
* @api {post} /api/:collection Create
* @apiName post:/api/:collection
* @apiVersion 0.0.1
* @apiGroup Restapi
* @apiDescription Create one new document
*
* @apiSuccess {Object} res Response.
* @apiSuccess {Object} res.ok 1 if success.
* @apiSuccess {Object} res.data Returned document.
*
* @apiError Db:DuplicateKey When a field set as unique index has been already created
* @apiUse SessionError
*/
router.post(`/${pColl.name}`, (req, res) => {
pColl.__services.create(req.body, req.user, res.done)
})
pColl.__services.create = function (pDoc, pUser, pCallback) {
collValidator(pDoc)
let errors = collValidator.errors
if (errors && errors.length) return pCallback(errors)
//
let tasks = []
let createdDoc = {ok: 0, doc: null}
tasks.push(function (pCb) {
pColl.hooks.create.before(pCtx, pDoc, function (pErr) {
pCb(pErr)
})
})
tasks.push(function (pCb) {
// delete pDoc._id
pColl.__addMetadata(pDoc, pUser)
collHandler.insertOne(pDoc, function (pErr, pResult) {
if (pErr) {
switch (pErr.code) {
case 11000:
createdDoc = pErr
return pCb(new Error('Db:DuplicateKey'), pErr)
default:
return pCb(pErr)
}
}
createdDoc.ok = pResult.insertedCount
createdDoc.doc = pResult.ops[0]
pCb()
})
})
tasks.push(function (pCb) {
pColl.hooks.create.after(pCtx, createdDoc.doc, function (pErr) {
pCb(pErr)
})
})
async.waterfall(tasks, function (pErr) {
pCallback(pErr, createdDoc)
})
}
/**
* @api {get} /api/:collection/:id Get one
* @apiName get:/api/:collection/:id
* @apiVersion 0.0.1
* @apiGroup Restapi
* @apiDescription Get one document
*
* @apiParam {String} collection Required collection
* @apiParam {ObjectId} id Required document Id
*
* @apiSuccess {Object} res Response.
* @apiSuccess {Object} res.ok 1 if success.
* @apiSuccess {Object} res.data Returned document.
*
* @apiError Db:WrongDbId ID parameter is not a MongoDB ObjectId
* @apiError Db:NotFound Required document ID not found in DB
* @apiUse SessionError
*/
router.get(`/${pColl.name}/:id`, (req, res) => {
pColl.__services.readOne(req.params.id, req.restOp, res.done)
})
pColl.__services.readOne = function (pId, pArgs, pCallback) {
// Convert ID if it's a correct one else returns one error
let objectId = getObjectId(pId, pColl.customId)
if (!objectId) {
return pCallback(new Error('Db:WrongDbId'))
}
let tasks = []
let oneDoc = {ok: 0, id: pId, data: null}
// Call before hook to transform data
tasks.push(function (pCb) {
pColl.hooks.readOne.before(pCtx, objectId, function (pErr) {
pCb(pErr)
})
})
// Get one document
tasks.push(function (pCb) {
collHandler
.find({_id: objectId})
.project(pArgs.projection)
.limit(1)
.next(function (pErr, pDoc) {
if (pErr) return pCallback(pErr)
if (!pDoc) return pCallback(400, new Error('Db:NotFound'))
oneDoc.ok = 1
oneDoc.label = pCtx.restapi[pColl.name].label
oneDoc.icon = pCtx.restapi[pColl.name].icon
oneDoc.description = pCtx.restapi[pColl.name].description
oneDoc.data = pDoc
pCb()
})
})
// Call after hook to perform some operations
tasks.push(function (pCb) {
pColl.hooks.readOne.after(pCtx, oneDoc.data, function (pErr) {
pCb(pErr)
})
})
async.waterfall(tasks, function (pErr) {
pCallback(pErr, oneDoc)
})
}
/**
* @api {put} /api/:collection/:id Update
* @apiName put:/api/:collection/:id
* @apiVersion 0.0.1
* @apiGroup Restapi
* @apiDescription Update some fields of one document
*
* @apiParam {String} collection Required collection
* @apiParam {ObjectId} id Required document Id
*
* @apiSuccess {Object} res Response.
* @apiSuccess {Object} res.ok 1 if success.
* @apiSuccess {Object} res.data Returned document.
*
* @apiError Db:WrongDbId ID parameter is not a MongoDB ObjectId
* @apiError Db:NotFound Required document ID not found in DB
* @apiUse SessionError
*/
router.put(`/${pColl.name}/:id`, (req, res) => {
pColl.__services.update(req.params.id, req.body, req.user, 'router', res.done)
})
pColl.__services.update = function (pId, pDoc, pUser, pOrigin, pCallback) {
// console.log(pDoc)
// Convert ID if it's a correct one else returns one error
let objectId = getObjectId(pId, pColl.customId)
if (!objectId) {
return pCallback(new Error('Db:WrongDbId'))
}
//
collValidator(pDoc)
if (collValidator.errors) {
// console.dir(collValidator.errors, {depth: null, colors: true})
let errors = collValidator.errors.filter(error => error.keyword !== 'required')
if (errors.length) return pCallback('JSONSchema:ValidationFailed', errors)
}
//
let tasks = []
let updatedDoc = { ok: 0, id: pId, doc: null }
// Call before hook to transform data
tasks.push(function (pCb) {
pColl.hooks.update.before(pCtx, objectId, pDoc, pOrigin, function (pErr) {
pCb(pErr)
})
})
// Save in DB
tasks.push(function (pCb) {
delete pDoc._id
// console.log(pDoc)
pColl.__updateMetadata(pDoc, pUser)
collHandler
.findOneAndUpdate(
{_id: objectId},
{$set: pDoc},
{returnOriginal: false},
function (pErr, pResult) {
if (pErr) return pCb(pErr)
updatedDoc.ok = pResult.ok
updatedDoc.doc = pResult.value
pCb()
})
})
// Call after hook to perform some operations
tasks.push(function (pCb) {
pColl.hooks.update.after(pCtx, updatedDoc.doc, pOrigin, function (pErr) {
pCb(pErr)
})
})
async.waterfall(tasks, function (pErr) {
pCallback(pErr, updatedDoc)
})
}
/**
* @api {delete} /api/:collection/:id Delete
* @apiName delete:/api/:collection/:id
* @apiVersion 0.0.1
* @apiGroup Restapi
* @apiDescription Delete one document
*
* @apiParam {String} collection Required collection
* @apiParam {ObjectId} id Required document Id
*
* @apiSuccess {Object} res Response.
* @apiSuccess {Object} res.id [DbId] of deleted document.
* @apiSuccess {Object} res.ok 1 if success.
*
* @apiError Db:WrongDbId ID parameter is not a MongoDB ObjectId
* @apiUse SessionError
*/
router.delete(`/${pColl.name}/:id`, (req, res) => {
pColl.__services.delete(req.params.id, req.user, res.done)
})
pColl.__services.delete = function (pId, pUser, pCallback) {
// Convert ID if it's a correct one else returns one error
let objectId = getObjectId(pId, pColl.customId)
if (!objectId) {
return pCallback(new Error('Db:WrongDbId'))
}
let tasks = []
let deletedDoc = {ok: 0, id: pId}
// Call before hook to transform data
tasks.push(function (pCb) {
pColl.hooks.delete.before(pCtx, objectId, function (pErr) {
pCb(pErr)
})
})
// Get one document
tasks.push(function (pCb) {
collHandler.removeOne({_id: objectId}, function (pErr, pDoc) {
if (pErr) return pCb(pErr)
deletedDoc.ok = pDoc.deletedCount
pCb()
})
})
// Call after hook to perform some operations
tasks.push(function (pCb) {
pColl.hooks.delete.after(pCtx, true, function (pErr) {
pCb(pErr)
})
})
async.waterfall(tasks, function (pErr) {
pCallback(pErr, deletedDoc)
})
}
/**
* @api {get} /api/:collection/private/json-schema Get json schema
* @apiName get:/api/:collection/private/json-schema
* @apiVersion 0.0.1
* @apiGroup Restapi
* @apiDescription Get JSON schema defined for this collection
*
* @apiParam {String} collection Required collection
*
* @apiSuccess {Json} schema JSON schema.
*
* @apiUse SessionError
*/
router.get(`/${pColl.name}/private/json-schema`, (req, res) => {
res.done(null, pColl.schema)
})
router.get(`/${pColl.name}/private/json-form`, (req, res) => {
res.done(null, pColl.form || [])
})
router.get(`/${pColl.name}/private/json-form-create`, (req, res) => {
res.done(null, pColl.formCreate || [])
})
router.get(`/${pColl.name}/private/listing-header`, (req, res) => {
res.done(null, pColl.listingHeader || [{ text: 'Nom', align: 'left', value: 'name' }])
})
router.get(`/${pColl.name}/private/new-objectid`, (req, res) => {
res.done(null, new ObjectId())
})
router.put(`/${pColl.name}/upsert/from/:field`, (req, res) => {
let findOp = {
[req.params.field]: req.body[req.params.field]
}
let cat = req.body.categories
delete req.body[req.params.value]
delete req.body.categories
let upOp = {$set: req.body}
if (cat) {
upOp['$addToSet'] = {categories: cat}
}
collHandler.findOneAndUpdate(
findOp,
upOp,
{upsert: true},
res.done)
})
/**
* @api {get} /api/:collection/private/json-schema/fake Get fake schema
* @apiName get:/api/:collection/private/json-schema/fake
* @apiVersion 0.0.1
* @apiGroup Restapi
* @apiDescription Get a fake JSON from schema defined for this collection
*
* @apiParam {String} collection Required collection
*
* @apiSuccess {Json} schema Fake JSON.
*
* @apiUse SessionError
*/
router.get(`/${pColl.name}/private/json-schema/fake`, (req, res) => {
jsf.option({ alwaysFakeOptionals: true })
jsf
.resolve(pColl.schema)
.then(function (result) {
if (result.planning) {
result.planning.byFormat = {
bannersHome: {
byAirline: {
AirFrance: {
deltaImpressions: 10000,
allowedFormats: ['iab-leaderboard', 'iab-squra']
},
TAROM: {
deltaImpressions: -10000,
allowedFormats: ['iab-squra']
}
}
}
}
}
res.done(null, result)
})
.catch(function (error) {
res.done(error)
})
})
}
function buildRestOpObject () {
return {
filter: {},
projection: {},
sort: {},
limit: 100,
skip: 0
}
}
function generateFilterObject (pReq) {
var filterObject = {}
var sortObject = {}
var projObject = {}
var request = null
var queries = pReq.query
var queryKeys = Object.keys(queries)
var length = queryKeys.length
var limit = 0
var skip = 0
var index, key, localData, lQueries, regArray, match, query, type
var deletedDocs = null
for (index = 0; index < length; index++) {
key = queryKeys[index]
if (key === 'limit') {
limit = parseInt(queries.limit)
} else if (key === 'skip') {
skip = parseInt(queries.skip)
} else if (key === 'projection') {
queries.projection.split(',').forEach(function (pProj) {
var projField = pProj.split(':')
projObject[projField[0]] = +projField[1]
})
} else if (key === 'sort') {
queries.sort.split(',').forEach(function (pSort) {
if (pSort[0] === '-') {
sortObject[pSort.substring(1)] = -1
} else {
sortObject[pSort] = 1
}
})
} else if (key === 'search') {
filterObject['$text'] = { '$search': queries.search }
} else if (key === 'deletedDocs') {
deletedDocs = queries.deletedDocs
} else {
request = key.split('__')
if (request.length <= 1 || !queries[key]) {
// console.log('unknown key', key);
continue
}
match = queries[key].match(/\((.*)\)(.*)/)
if (match) {
type = match[1]
query = castValue(match[2], type)
} else {
type = null
query = queries[key]
}
filterObject[request[0]] = filterObject[request[0]] || {}
switch (request[1]) {
case 'eq' :
if (!type) {
switch (query) {
case 'true':
query = true
break
case 'false':
query = false
break
default:
if (!isNaN(query)) {
query = +query
}
}
}
filterObject[request[0]]['$eq'] = query
break
case 'lt' :
filterObject[request[0]]['$lt'] = query
break
case 'lte' :
filterObject[request[0]]['$lte'] = query
break
case 'gt' :
filterObject[request[0]]['$gt'] = query
break
case 'gte' :
filterObject[request[0]]['$gte'] = query
break
case 'ne' :
filterObject[request[0]]['$ne'] = query
break
case 'regex' :
regArray = query.split(',')
filterObject[request[0]]['$regex'] = new RegExp(regArray[0], regArray[1])
break
case 'regexNot' :
regArray = query.split(',')
filterObject[request[0]]['$not'] = new RegExp(regArray[0], regArray[1])
break
case 'exists' :
filterObject[request[0]]['$exists'] = query == 'true'
break
case 'in' :
case 'all' :
case 'nin' :
if (typeof query === 'string') {
localData = query.split(',')
} else {
localData = [query]
}
if (!type && request[0] === '_id') {
type = 'ObjectId'
}
if (type) {
lQueries = []
localData.forEach(function (pElem) {
lQueries.push(castValue(pElem, type))
})
} else {
lQueries = localData
}
filterObject[request[0]]['$' + request[1]] = lQueries
break
default:
;
}
}
}
// if (!deletedDocs) {
// filterObject['__metadata.deletionDate'] = { $exists: false }
// } else {
// if (deletedDocs === 'only') {
// filterObject['__metadata.deletionDate'] = { $exists: true }
// }
// }
pReq.restOp = buildRestOpObject()
pReq.restOp.filter = filterObject
pReq.restOp.projection = projObject
pReq.restOp.sort = sortObject
pReq.restOp.limit = limit || 100
pReq.restOp.skip = skip
return pReq.restOp
}
function getObjectId (pId, pCustomId) {
let objectId = pCustomId ? pId : null
try { objectId = ObjectId(pId) } catch (e) {}
return objectId
}
function castValue (pValue, pType) {
var query = pValue
switch (pType) {
case 'Number':
query = +query
break
case 'Boolean':
query = query == 'true'
break
case 'Date':
query = new Date(query)
break
case 'ObjectId':
try {
query = ObjectId(query)
} catch (e) {
// query = query
}
break
case 'NULL':
query = null
break
default:
console.warn('Unknown cast type', pType)
}
return query
}
module.exports = function (pCtx) {
initCollections(pCtx)
return router
}

25
services/restapi/router.spec.js

@ -0,0 +1,25 @@
/* eslint-env mocha */
/* eslint no-unused-expressions: 0 */
const chai = require('chai')
const chaiHttp = require('chai-http')
chai.use(chaiHttp)
describe('Restapi - Router', function () {
require('./collections/users/users.spec')
require('./collections/airports/airports.spec')
require('./collections/media-plannings/media-plannings.spec')
require('./collections/advertisers/advertisers.spec')
require('./collections/airlines/airlines.spec')
require('./collections/airlines/airlines.services.spec')
require('./collections/languages/languages.spec')
require('./collections/languages/languages.services.spec')
})
// require('./router.languages.spec')

89
services/session/auth.js

@ -0,0 +1,89 @@
const jwt = require('jsonwebtoken')
const camelcase = require('camelcase')
const CFG = require('../../config')
const TOL = require('../../tools/common')
const ObjectId = require('mongodb').ObjectId
const MSG = {
USER: {
NOT_ALLOWED: 'User:NotAllowed'
}
}
/**
* For each received request, check the jwt token and user rights
*
* @param {HttpRequest} req Http request from client
* @param {HttpResponse} res Http context to answer to client
* @param {Function} next Callback to continue the Express pipe
*/
module.exports = function (pCtx) {
return (req, res, next) => {
if (req.method === 'OPTIONS') return next()
// Find token in the request. Can be set in:
// - header
// - token field : {token}
// - Authorization field : "Bearer {token}"
// - query
// - token field
let lToken
if (req.headers.token) {
lToken = req.headers.token
} else if (req.query.token) {
lToken = req.query.token
} else if (req.headers.authorization) {
let match = req.headers.authorization.match(/Bearer ?(.*)/)
if (match) {
lToken = match[1]
}
} else {
// To be removed when client is ready to use full auth API
if (!CFG.jwt.enable) {
return pCtx.db.collection('users')
.find({username: 'admin'})
.limit(1)
.next(function (pErr, pUser) {
// console.log(pUser)
req.user = pUser
next()
})
} else {
return res.done(401, 'jwtAccess:Missing')
}
}
// Decrypt received token
jwt.verify(lToken, CFG.jwt.jwtAccess.secret, function (pErr, pToken) {
if (pErr) {
return res.done(401, new Error(`jwtAccess:${camelcase(pErr.message, {pascalCase: true})}`))
}
res.token = pToken
let payloadState = TOL.jwt.checkPayload(req, res.token.data)
if (payloadState) {
return res.done(401, 'jwtAccess:CorruptedPayload:' + payloadState)
}
req.user = res.token.data.user
try { req.user._id = ObjectId(req.user.id) } catch (e) {}
delete req.user.pass
next()
})
}
}
/**
* Projects user fields to be public compliant
* @param {Object} pUser Public-ified user
*/
function JOB_getPublicUser (pUser) {
return {
id: pUser.id,
user: pUser.user,
role: pUser.role,
name: pUser.name
}
}

118
services/session/router.js

@ -0,0 +1,118 @@
const express = require('express')
const router = express.Router()
const auth = require('../session/auth')
// const jwt = require('jsonwebtoken')
module.exports = function (pCtx) {
const services = require('./services')(pCtx)
/**
* @api {post} /session/login User Login
* @apiName post:/session/login
* @apiVersion 0.0.1
* @apiGroup Session
* @apiDescription Api for User login
* Credientials must be set in a json body
*
* @apiParam {String} user Username for login
* @apiParam {String} pass Password for login
*
* @apiParamExample {json} Login Example:
* {
* "user": "User name",
* "pass": "User plain password",
* }
*
* @apiSuccess {Object} SessionCtx Context of successful login
* @apiSuccess {String} SessionCtx.jwtAuth Authentification token to use for get one jwtAccess token - Should be saved locally and set in header authorization field
* @apiSuccess {String} SessionCtx.user Just a reminder for user
*
* @apiError User:NotFound Username or email not found
* @apiError User:WrongPassword User found but password does not match
*/
router.post('/login', services.login)
/**
* @api {post} /session/access User Acces
* @apiName post:/session/access
* @apiVersion 0.0.1
* @apiGroup Session
* @apiDescription Get an jwtAccess from jwtAuth to access to all API with a short period
* Futermore, jwtAuth token TTL is updated
*
* @apiParam {String} jwtAuth jwtAuth token generated from /session/login
*
* @apiParamExample {json} jwtAuth:
* {
* "jwtAuth": "[Authentification JWT token]",
* }
*
* @apiSuccess {Object} SessionCtx Context of successful login
* @apiSuccess {String} SessionCtx.jwtAuth Updated jwtAuth token
* @apiSuccess {String} SessionCtx.jwtAccess Access token to use for each request to server - Should be saved locally and set in header authorization field
*
* @apiError User:NotFound Username or email not found
* @apiError User:PasswordChanged User found but password does not match from login one
* @apiError jwtAuth:Missing jwtAuth is missing in body request
* @apiError jwtAuth:InvalidSignature Signature of jwtAuth is invalid
* @apiError jwtAuth:JwtExpired Token is expired. A login try to login again
* @apiError jwtAuth:CorruptedPayload Token payload is corrupted (server version changes, wrong userAgent ou client ip address)
*/
// router.post('/access', services.access)
router.use(auth(pCtx))
/**
* @api {get} /session/me About me
* @apiName get:/session/me
* @apiVersion 0.0.1
* @apiGroup Session
* @apiDescription Get information about user of jwtAccess token
* Common case: check if user has still has access rights
*
* @apiParam {Header} token jwtAccess token
*
* @apiSuccess {Object} res Response
* @apiSuccess {String} res.user User information
*
* @apiUse SessionError
*/
router.get('/me', services.me)
/**
* @api {get} /session/me/filters My filters - Get
* @apiName get:/session/me/filters
* @apiVersion 0.0.1
* @apiGroup Session
* @apiDescription Get all custom filters created by current user
*
* @apiParam {Header} token jwtAccess token
*
* @apiSuccess {Object} res Response
* @apiSuccess {String} res.data All filters
*
* @apiUse SessionError
*/
router.get('/me/filters', services.filtersGet)
/**
* @api {post} /session/me/filters My filters - Create
* @apiName post:/session/me/filters
* @apiVersion 0.0.1
* @apiGroup Session
* @apiDescription Create new custom filter for current user
*
* @apiParam (Header) {String} token jwtAccess token
* @apiParam (Body) {Object} filter New Filter to create
* @apiParam (Body) {String} filter.name Name of filter
* @apiParam (Body) {Object} filter.rules Rules to build this filter
*
* @apiSuccess {Object} res Response
* @apiSuccess {String} res.doc New Filter
*
* @apiUse SessionError
*/
router.post('/me/filters', services.filtersCreate)
return router
}

171
services/session/router.spec.js

@ -0,0 +1,171 @@
/* eslint-env mocha */
/* eslint no-unused-expressions: 0 */
const chai = require('chai')
const chaiHttp = require('chai-http')
const expect = chai.expect
const jwt = require('jsonwebtoken')
const CFG = require('../../config')
chai.use(chaiHttp)
describe('Session - Router', function () {
describe('# login', function () {
it('POST /session/login with unknown user', function (done) {
chai.request(global.server)
.post('/session/login')
.send({user: 'airpmpA', pass: 'admin'})
.end((err, res) => {
expect(err).to.be.null
expect(res).to.have.status(401)
expect(res.body).to.have.property('error', 'User:NotFound')
done()
})
})
it('POST /session/login with wrong password', function (done) {
chai.request(global.server)
.post('/session/login')
.send({user: 'airpmp', pass: 'toto'})
.end((err, res) => {
expect(err).to.be.null
expect(res).to.have.status(401)
expect(res.body).to.have.property('error', 'User:WrongPassword')
done()
})
})
it('POST /session/login', function (done) {
chai.request(global.server)
.post('/session/login')
.send(global.login)
.end((err, res) => {
// console.log(res.body)
expect(err).to.be.null
expect(res).to.have.status(200)
expect(res.body).to.have.nested.property('user.username', global.login.user)
expect(res.body).to.have.property('jwtAuth')
global.jwtAuth = res.body.jwtAuth
done()
})
})
it('POST /session/access with no jwtAuth', function (done) {
chai.request(global.server)
.post('/session/access')
.send({})
.end((err, res) => {
expect(err).to.be.null
expect(res).to.have.status(401)
done()
})
})
it('POST /session/access', function (done) {
chai.request(global.server)
.post('/session/access')
.send({jwtAuth: global.jwtAuth})
.end((err, res) => {
expect(err).to.be.null
expect(res).to.have.status(200)
expect(res.body).to.have.property('jwtAuth')
expect(res.body).to.have.property('jwtAccess')
let clearToken = jwt.decode(res.body.jwtAccess)
expect(clearToken).to.have.nested.property('data.user.rights')
global.jwtAuth = res.body.jwtAuth
global.jwtAccess = res.body.jwtAccess
done()
})
})
it('POST /session/access with updated jwtAuth', function (done) {
chai.request(global.server)
.post('/session/access')
.send({jwtAuth: global.jwtAuth})
.end((err, res) => {
expect(err).to.be.null
expect(res).to.have.status(200)
expect(res.body).to.have.property('jwtAuth')
expect(res.body).to.have.property('jwtAccess')
global.jwtAuth = res.body.jwtAuth
global.jwtAccess = res.body.jwtAccess
done()
})
})
})
describe('# session me', function () {
it('GET /session/me with no jwtAccess', function (done) {
chai.request(global.server)
.get('/session/me')
.end((err, res) => {
expect(err).to.be.null
if (CFG.jwt.enable) {
expect(res).to.have.status(401)
}
done()
})
})
it('GET /session/me', function (done) {
chai.request(global.server)
.get('/session/me')
.set('token', global.jwtAccess)
.end((err, res) => {
// console.log(res.body)
expect(err).to.be.null
expect(res.body).to.not.have.property('error')
expect(res).to.have.status(200)
done()
})
})
})
describe('# session - my filters', function () {
let createdFilter = null
it('GET /session/me/filters', function (done) {
chai.request(global.server)
.get('/session/me/filters')
.set('token', global.jwtAccess || '')
.end((err, res) => {
// console.log(res.body)
expect(err).to.be.null
// expect(res.body).to.have.property('data')
// expect(res.body).to.not.have.property('error')
expect(res).to.have.status(200)
done()
})
})
it('CREATE /session/me/filters', function (done) {
chai.request(global.server)
.post('/session/me/filters')
.set('token', global.jwtAccess || '')
.send({name: 'test-test', rules: {test: ''}})
.end((err, res) => {
expect(err).to.be.null
// expect(res.body).to.have.property('data')
// expect(res.body).to.not.have.property('error')
expect(res).to.have.status(200)
createdFilter = res.body.doc
done()
})
})
it('DELETE users-filters by api', function (done) {
chai.request(global.server)
.delete('/api/users-filters/' + createdFilter._id)
.set('token', global.jwtAccess || '')
.end((err, res) => {
expect(err).to.be.null
// expect(res.body).to.have.property('data')
// expect(res.body).to.not.have.property('error')
expect(res).to.have.status(200)
done()
})
})
})
})
// https://scotch.io/tutorials/test-a-node-restful-api-with-mocha-and-chai

107
services/session/services.js

@ -0,0 +1,107 @@
/* eslint operator-linebreak: 0 */
const jwt = require('jsonwebtoken')
const camelcase = require('camelcase')
const ObjectId = require('mongodb').ObjectId
const CFG = require('../../config')
const TOL = require('../../tools/common')
module.exports = function (pCtx) {
const services = {}
let dbUsers = pCtx.db.collection('users')
services.login = function (req, res, next) {
dbUsers
.find({username: req.body.user})
.limit(1)
.next(function (pErr, pUser) {
if (pErr) {
return res.done(555, pErr)
}
if (!pUser) {
return res.done(401, new Error('User:NotFound'))
}
if (!pUser.multipass || pUser.multipass.hash !== req.body.pass) {
return res.done(401, new Error('User:WrongPassword'))
}
let payload = buildDefaultPayload(req, pUser)
let token = jwt.sign({data: payload}, CFG.jwt.jwtAuth.secret, { expiresIn: CFG.jwt.jwtAuth.expiresIn })
res.done(null, {user: pUser, token: token})
})
}
// services.access = function (req, res, next) {
// if (!req.body.jwtAuth) {
// return res.done(401, new Error('jwtAuth:Missing'))
// }
// jwt.verify(req.body.jwtAuth, CFG.jwt.jwtAuth.secret, function (pErr, pDecoded) {
// if (pErr) {
// return res.done(401, new Error(`jwtAuth:${camelcase(pErr.message, {pascalCase: true})}`))
// }
// let payloadState = TOL.jwt.checkPayload(req, pDecoded.data)
// if (payloadState) {
// return res.done(401, 'jwtAuth:CorruptedPayload:' + payloadState)
// }
// dbUsers
// .find({_id: ObjectId(pDecoded.data.user.id)})
// .limit(1)
// .next(function (pErr, pUser) {
// if (pErr || !pUser) {
// return res.done(401, 'User:NotFound')
// }
// let hashPass = TOL.createHash(pUser.multipass.hash, null, CFG.jwt.jwtAuth.passSalt)
// if (pDecoded.data.user.pass !== hashPass) {
// return res.done(401, 'User:PasswordChanged')
// }
// let payload = buildDefaultPayload(req, pUser)
// let jwtAuth = jwt.sign({data: payload}, CFG.jwt.jwtAuth.secret, { expiresIn: CFG.jwt.jwtAuth.expiresIn })
// payload.user.rights = pUser.rights
// let jwtAccess = jwt.sign({data: payload}, CFG.jwt.jwtAccess.secret, { expiresIn: CFG.jwt.jwtAccess.expiresIn })
// res.done(null, {jwtAuth, jwtAccess})
// })
// })
// }
services.me = function (req, res, next) {
res.done(null, {user: req.user})
}
services.filtersGet = function (req, res, next) {
let restOp = pCtx.restapi['users-filters'].__buildRestOp()
restOp.filter = { '__metadata.owner': req.user._id }
pCtx.restapi['users-filters'].__services.read(restOp, res.done)
}
services.filtersCreate = function (req, res, next) {
pCtx.restapi['users-filters'].__services.create(req.body, req.user, res.done)
}
return services
}
function buildDefaultPayload (req, pUser) {
return {
user: {
id: pUser._id,
username: pUser.username,
details: pUser.details,
pass: TOL.createHash(pUser.multipass.hash, null, CFG.jwt.jwtAuth.passSalt)
},
sid: CFG.jwt.sid,
clientInfo: TOL.buildClientInfo(req)
}
}

70
services/session/services.spec.js

@ -0,0 +1,70 @@
/* eslint-env mocha */
const mongoClient = require('mongodb').MongoClient
const CFG = require('../../config')
const CTX = {
db: {}
}
const services = require('./services')(CTX)
const req = {}
const res = {
_done: function () {
console.error('Mocha done function not set')
process.exit(1)
},
setDone: function (done) {
this._done = done
},
done: function (status, err, json) {
typeof status === 'number'
? this._done(status, err, json)
: this._done(555, status, err)
}
}
describe('Session - Services', function () {
// LOGIN ////////////////////////////////////////////////////////////////////
describe('#login', function () {
before(function (done) {
mongoClient.connect(CFG.mongo.url, { useNewUrlParser: true }, function (pErr, pDbHandler) {
if (pErr) {
process.exit(1)
}
CTX.db.handler = pDbHandler
CTX.db.airpmp = CTX.db.handler.db('airpmp')
done()
})
})
// it('should login with success', function (done) {
// req.body = {
// user: 'airpmp',
// pass: 'admin'
// }
// res.setDone(function (pStatus, pErr, pResult) {
// if (pErr) return done(pErr)
// if (pResult.user.username !== req.body.user) return done(new Error('CorruptedUser'))
// done()
// })
// services.login(req, res)
// })
// it('should not login with wrong credential', function (done) {
// req.body = {
// user: 'test',
// pass: 'adminz'
// }
// res.setDone(function (pStatus, pErr, pResult) {
// if (pErr) return done()
// done(new Error('LoggedWithWrongCreditential'))
// })
// services.login(req, res)
// })
after(function (done) {
CTX.db.handler.close()
done()
})
})
})

43
tools/common.js

@ -0,0 +1,43 @@
const crypto = require('crypto')
const exec = require('child_process').exec
const CFG = require('../config')
const tools = {}
tools.createHash = function (pData, pHash, pSalt) {
let hash = pHash || 'md5'
let data = typeof pData === 'object' ? JSON.stringify(pData) : pData
if (pSalt) {
data = `${data}$-|-$${pSalt}`
}
return crypto.createHash(hash).update(data).digest('hex')
}
tools.buildClientInfo = function (req) {
return {
uaHash: tools.createHash(req.headers['user-agent']),
ip: req.clientIp
}
}
tools.jwt = {}
tools.jwt.checkPayload = function (req, pPayload) {
let clientInfo = tools.buildClientInfo(req)
if (!pPayload.user) return 'user'
if (!pPayload.user.id) return 'user.id'
if (!pPayload.user.pass) return 'user.pass'
if (!pPayload.clientInfo) return 'clientInfo'
if (pPayload.clientInfo.ip !== clientInfo.ip) return 'clientInfo.ip'
if (pPayload.clientInfo.uaHash !== clientInfo.uaHash) return 'clientInfo.uaHash'
if (pPayload.sid !== CFG.jwt.sid) return 'clientInfo.sid'
return null
}
tools.git = {}
tools.git.currentRevision = '---'
exec('git rev-parse --short HEAD | xargs echo -n', function (pErr, pStdout) {
if (pStdout) tools.git.currentRevision = pStdout
})
module.exports = tools

53
tools/middleware-done.js

@ -0,0 +1,53 @@
const requestIp = require('request-ip')
module.exports = function (req, res, next) {
// Website you wish to allow to connect
res.setHeader('Access-Control-Allow-Origin', '*')
// Request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE')
// Request headers you wish to allow
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type,token,authorization')
//
req.clientIp = requestIp.getClientIp(req)
//
res.done = (pStatus, pErr, pJson) => {
let status = null
let error = null
let json = null
if (typeof pStatus === 'number') {
status = pStatus
error = pErr
json = pJson || {}
} else {
error = pStatus
json = pErr || {}
}
if (!error && status >= 400) {
error = 'Error:401'
}
if (error) {
status = status || 555
let errorToSend = {
error: '',
httpStatus: status,
stack: null,
more: json || {}
}
if (!(error instanceof Error)) {
if (typeof error === 'object') error = JSON.stringify(error)
error = new Error(error)
}
errorToSend.error = error.message
errorToSend.stack = error.stack
res.status(status).json(errorToSend)
} else {
res.json(json)
}
}
next()
}

22
tools/time-profiler.js

@ -0,0 +1,22 @@
module.exports = class TimeProfiler {
constructor (pTag, pHideIt) {
this.hideIt = !!pHideIt
this.tag = pTag || 'TimeProfile'
this.list = [{title: 'Init', date: new Date()}]
}
tick (pTitle) {
this.list.push({title: pTitle, date: new Date()})
}
done () {
if (this.hideIt) return
console.log('Profiling results for:' + this.tag)
console.log(' - ' + this.list[0].title + ': 0')
for (var i = 1; i < this.list.length; i++) {
console.log(' - ' + this.list[i].title + ': ' + (this.list[i].date - this.list[i - 1].date))
}
console.log(' - TOTAL', (new Date()) - this.list[0].date)
this.list = []
}
}
Loading…
Cancel
Save