34 changed files with 6710 additions and 0 deletions
Unified View
Diff Options
-
78service/.gitignore
-
26service/Dockerfile
-
2service/README.md
-
1service/VERSION
-
7service/apidoc.js
-
54service/config.js
-
121service/index.js
-
23service/mocha.entry.js
-
4305service/package-lock.json
-
55service/package.json
-
8service/services/core/config/router.js
-
24service/services/core/config/router.spec.js
-
201service/services/media/router.js
-
BINservice/services/media/rsc/default/404.jpg
-
70service/services/restapi/collections/__template.js
-
36service/services/restapi/collections/config/config.form.js
-
18service/services/restapi/collections/config/config.js
-
38service/services/restapi/collections/landingpages/landingpages.form.js
-
32service/services/restapi/collections/landingpages/landingpages.js
-
21service/services/restapi/collections/media/media.js
-
14service/services/restapi/collections/users-filters/users-filters.js
-
58service/services/restapi/collections/users/users.form.js
-
47service/services/restapi/collections/users/users.js
-
39service/services/restapi/collections/users/users.spec.js
-
734service/services/restapi/router.js
-
25service/services/restapi/router.spec.js
-
89service/services/session/auth.js
-
118service/services/session/router.js
-
171service/services/session/router.spec.js
-
107service/services/session/services.js
-
70service/services/session/services.spec.js
-
43service/tools/common.js
-
53service/tools/middleware-done.js
-
22service/tools/time-profiler.js
@ -0,0 +1,78 @@ |
|||||
|
# ---> Node |
||||
|
# Logs |
||||
|
logs |
||||
|
*.log |
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
|
|
||||
|
# Runtime data |
||||
|
pids |
||||
|
*.pid |
||||
|
*.seed |
||||
|
*.pid.lock |
||||
|
|
||||
|
# Directory for instrumented libs generated by jscoverage/JSCover |
||||
|
lib-cov |
||||
|
|
||||
|
# Coverage directory used by tools like istanbul |
||||
|
coverage |
||||
|
|
||||
|
# nyc test coverage |
||||
|
.nyc_output |
||||
|
|
||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) |
||||
|
.grunt |
||||
|
|
||||
|
# Bower dependency directory (https://bower.io/) |
||||
|
bower_components |
||||
|
|
||||
|
# node-waf configuration |
||||
|
.lock-wscript |
||||
|
|
||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html) |
||||
|
build/Release |
||||
|
|
||||
|
# Dependency directories |
||||
|
node_modules/ |
||||
|
jspm_packages/ |
||||
|
|
||||
|
# TypeScript v1 declaration files |
||||
|
typings/ |
||||
|
|
||||
|
# Optional npm cache directory |
||||
|
.npm |
||||
|
|
||||
|
# Optional eslint cache |
||||
|
.eslintcache |
||||
|
|
||||
|
# Optional REPL history |
||||
|
.node_repl_history |
||||
|
|
||||
|
# Output of 'npm pack' |
||||
|
*.tgz |
||||
|
|
||||
|
# Yarn Integrity file |
||||
|
.yarn-integrity |
||||
|
|
||||
|
# dotenv environment variables file |
||||
|
.env |
||||
|
|
||||
|
# parcel-bundler cache (https://parceljs.org/) |
||||
|
.cache |
||||
|
|
||||
|
# next.js build output |
||||
|
.next |
||||
|
|
||||
|
# nuxt.js build output |
||||
|
.nuxt |
||||
|
|
||||
|
# vuepress build output |
||||
|
.vuepress/dist |
||||
|
|
||||
|
# Serverless directories |
||||
|
.serverless |
||||
|
|
||||
|
# FuseBox cache |
||||
|
.fusebox/ |
||||
|
|
||||
@ -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"] |
||||
@ -0,0 +1,2 @@ |
|||||
|
# nubium-artifex-service |
||||
|
|
||||
@ -0,0 +1 @@ |
|||||
|
|
||||
@ -0,0 +1,7 @@ |
|||||
|
// ////////////////////////////////////////////////////////////////////////////
|
||||
|
// Current Errors.
|
||||
|
// ////////////////////////////////////////////////////////////////////////////
|
||||
|
|
||||
|
// ////////////////////////////////////////////////////////////////////////////
|
||||
|
// Current Params.
|
||||
|
// ////////////////////////////////////////////////////////////////////////////
|
||||
@ -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 |
||||
@ -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 () {}) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
@ -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
service/package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
const express = require('express') |
||||
|
const router = express.Router() |
||||
|
|
||||
|
const CFG = require('../../../config') |
||||
|
|
||||
|
module.exports = function (pCtx) { |
||||
|
return router |
||||
|
} |
||||
@ -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() |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -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 |
||||
|
} |
||||
@ -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) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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' } |
||||
|
] |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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' } |
||||
|
] |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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' } |
||||
|
] |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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() |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -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 |
||||
|
} |
||||
@ -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')
|
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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
|
||||
@ -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) |
||||
|
} |
||||
|
} |
||||
@ -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() |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -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 |
||||
@ -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() |
||||
|
} |
||||
@ -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 = [] |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save