34 changed files with 6710 additions and 0 deletions
Split 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