From f23b65d3d8e5c8343d7d5c12103091c8024ca67b Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Wed, 22 Apr 2020 17:24:15 +0200 Subject: [PATCH 01/16] implemented first tests and basic functionality --- .idea/codeStyles/codeStyleConfig.xml | 5 ++ .idea/dictionaries/VLE2FE.xml | 9 +++ oas/oas.yaml | 9 +++ oas/schemas.yaml | 16 ++--- oas/user.yaml | 14 +++-- package-lock.json | 48 +++++++++++++++ package.json | 3 + src/db.ts | 90 ++++++++++++++++++++++++++++ src/globals.ts | 11 ++++ src/index.ts | 56 ++++++++--------- src/models/user.ts | 13 ++++ src/routes/root.spec.ts | 53 +++++++++++++--- src/routes/user.spec.ts | 75 +++++++++++++++++++++++ src/routes/user.ts | 31 ++++++++++ src/routes/validate/user.ts | 44 ++++++++++++++ src/test/db.json | 17 ++++++ tsconfig.json | 6 +- 17 files changed, 451 insertions(+), 49 deletions(-) create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/dictionaries/VLE2FE.xml create mode 100644 src/db.ts create mode 100644 src/globals.ts create mode 100644 src/models/user.ts create mode 100644 src/routes/user.spec.ts create mode 100644 src/routes/user.ts create mode 100644 src/routes/validate/user.ts create mode 100644 src/test/db.json diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/dictionaries/VLE2FE.xml b/.idea/dictionaries/VLE2FE.xml new file mode 100644 index 0000000..c274b8b --- /dev/null +++ b/.idea/dictionaries/VLE2FE.xml @@ -0,0 +1,9 @@ + + + + bcrypt + cfenv + dfopdb + + + \ No newline at end of file diff --git a/oas/oas.yaml b/oas/oas.yaml index 81e06bf..ba1bafd 100644 --- a/oas/oas.yaml +++ b/oas/oas.yaml @@ -15,6 +15,15 @@ info:
  • dev: handling machine learning models
  • admin: user administration
  • + Password policy: + diff --git a/oas/schemas.yaml b/oas/schemas.yaml index 21ebae4..82c9508 100644 --- a/oas/schemas.yaml +++ b/oas/schemas.yaml @@ -137,8 +137,6 @@ Template: type: object Email: - required: - - email properties: email: type: string @@ -151,14 +149,16 @@ User: name: type: string example: johndoe - levels: - type: array - items: - type: string - example: read + pass: + type: string + writeOnly: true + example: Abc123!# + level: + type: string + example: read location: type: string example: Rng device_name: type: string - example: Alpha II \ No newline at end of file + example: Alpha II diff --git a/oas/user.yaml b/oas/user.yaml index c9f10b1..f8434f5 100644 --- a/oas/user.yaml +++ b/oas/user.yaml @@ -130,16 +130,22 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/User' + required: + - email + - name + - pass + - level + - location + - device_name + allOf: + - $ref: 'oas.yaml#/components/schemas/User' responses: 200: description: user details content: application/json: schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'oas.yaml#/components/schemas/User' 400: $ref: 'oas.yaml#/components/responses/400' 401: diff --git a/package-lock.json b/package-lock.json index ae887b0..956d6f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,49 @@ "js-tokens": "^4.0.0" } }, + "@hapi/address": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.0.1.tgz", + "integrity": "sha512-0oEP5UiyV4f3d6cBL8F3Z5S7iWSX39Knnl0lY8i+6gfmmIBj44JCBNtcMgwyS+5v7j3VYavNay0NFHDS+UGQcw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@hapi/formula": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz", + "integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==" + }, + "@hapi/hoek": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.0.4.tgz", + "integrity": "sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw==" + }, + "@hapi/joi": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-17.1.1.tgz", + "integrity": "sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg==", + "requires": { + "@hapi/address": "^4.0.1", + "@hapi/formula": "^2.0.0", + "@hapi/hoek": "^9.0.0", + "@hapi/pinpoint": "^2.0.0", + "@hapi/topo": "^5.0.0" + } + }, + "@hapi/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==" + }, + "@hapi/topo": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", + "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@jsdevtools/ono": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz", @@ -165,6 +208,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, "binary-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", diff --git a/package.json b/package.json index 5c89ff2..234bb65 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,11 @@ "license": "ISC", "dependencies": { "@apidevtools/json-schema-ref-parser": "^8.0.0", + "@hapi/joi": "^17.1.1", "@types/mocha": "^5.2.7", "@types/node": "^13.1.6", + "bcryptjs": "^2.4.3", + "body-parser": "^1.19.0", "cfenv": "^1.2.2", "express": "^4.17.1", "json-schema": "^0.2.5", diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 0000000..1ac4222 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,90 @@ +import mongoose from 'mongoose'; +import cfenv from 'cfenv'; + +// database urls, prod db url is retrieved automatically +const TESTING_URL = 'mongodb://localhost/dfopdb_test'; +const DEV_URL = 'mongodb://localhost/dfopdb'; + +export default class db { + private static state = { // db object and current mode (test, dev, prod) + db: null, + mode: null, + }; + + static connect (mode = '', done: Function = () => {}) { // set mode to test for unit/integration tests, otherwise skip parameter. done is also only needed for testing + if (this.state.db) return done(); // db is already connected + + // find right connection url + let connectionString: string = ""; + if (mode === 'test') { // testing + connectionString = TESTING_URL; + this.state.mode = 'test'; + } + else if(process.env.NODE_ENV === 'production') { + let services = cfenv.getAppEnv().getServices(); + for (let service in services) { + if(services[service].tags.indexOf("mongodb") >= 0) { + connectionString = services[service]["credentials"].uri; + } + } + this.state.mode = 'prod'; + } + else { + connectionString = DEV_URL; + this.state.mode = 'dev'; + } + + // connect to db + mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true}, err => { + if (err) done(err); + }); + mongoose.connection.on('error', console.error.bind(console, 'connection error:')); + mongoose.connection.once('open', () => { + console.log(`Connected to ${connectionString}`); + this.state.db = mongoose.connection; + done(); + }); + } + + static getState () { + return this.state; + } + + static drop (done: Function = () => {}) { // drop all collections of connected db (only dev and test for safety reasons ;) + if (!this.state.db || this.state.mode === 'prod') return done(); // no db connection or prod db + this.state.db.db.listCollections().toArray((err, collections) => { // get list of all collections + if (collections.length === 0) { // there are no collections to drop + return done(); + } + else { + let dropCounter = 0; // count number of dropped collections to know when to return done() + collections.forEach(collection => { // drop each collection + this.state.db.dropCollection(collection.name, () => { + console.log('dropped collection ' + collection.name); + if (++ dropCounter >= collections.length) { // all collections dropped + done(); + } + }); + }); + } + }); + } + + static loadJson (json, done: Function = () => {}) { // insert given JSON data into db, uses core mongodb methods + if (!this.state.db || !json.hasOwnProperty('collections') || json.collections.length === 0) { + return done(); + } // no db connection or nothing to load + + let loadCounter = 0; // count number of loaded collections to know when to return done() + Object.keys(json.collections).forEach(collectionName => { // create each collection + this.state.db.createCollection(collectionName, (err, collection) => { + collection.insertMany(json.collections[collectionName], () => { // insert JSON data + console.log('loaded collection ' + collectionName); + if (++ loadCounter >= Object.keys(json.collections).length) { // all collections loaded + done(); + } + }); + }); + }); + } +}; \ No newline at end of file diff --git a/src/globals.ts b/src/globals.ts new file mode 100644 index 0000000..e6db442 --- /dev/null +++ b/src/globals.ts @@ -0,0 +1,11 @@ +const globals = { + levels: [ + 'read', + 'write', + 'maintain', + 'dev', + 'admin' + ] +}; + +export default globals; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 09fb57f..8c4af39 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,37 +1,16 @@ -import cfenv from 'cfenv'; import express from 'express'; -import mongoose from 'mongoose'; +import bodyParser from 'body-parser'; import swagger from 'swagger-ui-express'; import jsonRefParser, {JSONSchema} from '@apidevtools/json-schema-ref-parser'; +import db from './db'; // tell if server is running in debug or production environment console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : '===== DEVELOPMENT ====='); -// get mongodb address from server, otherwise set to localhost -let connectionString: string = ""; -if(process.env.NODE_ENV === 'production') { - let services = cfenv.getAppEnv().getServices(); - for (let service in services) { - if(services[service].tags.indexOf("mongodb") >= 0) { - connectionString = services[service]["credentials"].uri; - } - } -} -else { - connectionString = 'mongodb://localhost/dfopdb'; -} -mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true}); - -// connect to mongodb -let db = mongoose.connection; -db.on('error', console.error.bind(console, 'connection error:')); -db.once('open', () => { - console.log(`Connected to ${connectionString}`); -}); - - +// mongodb connection +db.connect(); // create Express app const app = express(); @@ -40,8 +19,17 @@ app.disable('x-powered-by'); // get port from environment, defaults to 3000 const port = process.env.PORT || 3000; +//middleware +app.use(express.json({ limit: '5mb'})); +app.use(express.urlencoded({ extended: false, limit: '5mb' })); +app.use(bodyParser.json()); +app.use((err, req, res, ignore) => { // bodyParser error handling + res.status(400).send({status: 'Invalid JSON body'}); +}); + // require routes app.use('/', require('./routes/root')); +app.use('/', require('./routes/user')); // Swagger UI let oasDoc: JSONSchema = {}; @@ -53,7 +41,19 @@ jsonRefParser.bundle('oas/oas.yaml', (err, doc) => { }); app.use('/api', swagger.serve, swagger.setup(oasDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'})); -// hook up server to port -app.listen(port, () => { - console.log(`Listening on http;//localhost:${port}`); +app.use((req, res) => { // 404 error handling + res.status(404).json({status: 'Not found'}); }); + +app.use((err, req, res, ignore) => { // internal server error handling + console.error(err); + res.status(500).json({status: 'Internal server error'}); +}); + + +// hook up server to port +const server = app.listen(port, () => { + console.log(`Listening on http://localhost:${port}`); +}); + +module.exports = server; \ No newline at end of file diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..b72dabb --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,13 @@ +import mongoose from 'mongoose'; + +const UserSchema = new mongoose.Schema({ + name: String, + email: String, + pass: String, + key: String, + level: String, + location: String, + device_name: String +}); + +export default mongoose.model('user', UserSchema); \ No newline at end of file diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index cfec79c..0e2d625 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -1,19 +1,58 @@ import supertest from 'supertest'; import should from 'should/as-function'; +import db from '../db'; -let server = supertest.agent('http://localhost:3000'); +describe('/', () => { + let server; -describe('Testing /', () => { - it('returns the message object', done => { - server + before(done => { + process.env.port = '2999'; + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns the root message', done => { + supertest(server) .get('/') .expect('Content-type', /json/) - .expect(200) - .end(function(err, res) { - should(res.statusCode).equal(200); + .expect(200, (err, res) => { should(res.body).be.eql({message: 'API server up and running!'}); done(); }); }); }); + +describe('Testing unknown routes', () => { + let server; + + before(done => { + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns a 404 message', done => { + supertest(server) + .get('/unknownroute') + .expect(404); + done(); + }); +}); \ No newline at end of file diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts new file mode 100644 index 0000000..fe5c031 --- /dev/null +++ b/src/routes/user.spec.ts @@ -0,0 +1,75 @@ +import supertest from 'supertest'; +import should from 'should/as-function'; +import db from '../db'; +import userModel from '../models/user'; + + +describe('/user/new', () => { + let server; + + before(done => { + process.env.port = '2999'; + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns the added user data', done => { + supertest(server) + .post('/user/new') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) + .expect('Content-type', /json/) + .expect(200, (err, res) => { + if (err) return done(err); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'john.doe@bosch.com'); + should(res.body).have.property('name', 'johndoe'); + should(res.body).have.property('level', 'read'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha II'); + done(); + }); + }); + it('stores the data', done => { + supertest(server) + .post('/user/new') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) + .expect(200, err => { + if (err) return done(err); + userModel.find({name: 'johndoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'johndoe'); + should(data[0]).have.property('email', 'john.doe@bosch.com'); + should(data[0]).have.property('pass').not.eql('Abc123!#'); + should(data[0]).have.property('level', 'read'); + should(data[0]).have.property('location', 'Rng'); + should(data[0]).have.property('device_name', 'Alpha II'); + done(); + }); + }); + }); + it('rejects a username already in use', done => { + supertest(server) + .post('/user/new') + .send({email: 'j.doe@bosch.com', name: 'janedoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) + .expect(400, err => { + if (err) return done(err); + userModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + done(); + }); + }); + }); // TODO: authentication +}); \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts new file mode 100644 index 0000000..ea2994f --- /dev/null +++ b/src/routes/user.ts @@ -0,0 +1,31 @@ +import express from 'express'; +import mongoose from 'mongoose'; +import bcrypt from 'bcryptjs'; +import UserValidate from './validate/user'; +import UserModel from '../models/user'; + +const router = express.Router(); + +router.get('/users', (req, res) => { + res.json({message: 'users up and running!'}); +}); + +router.post('/user/new', (req, res, next) => { + // validate input + const {error, value: user} = UserValidate.input(req.body); + if(error !== undefined) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + user.key = mongoose.Types.ObjectId(); // use object id as unique API key + bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing + user.pass = hash; + new UserModel(user).save((err, data) => { // store user + if (err) next(err); + res.json(UserValidate.output(data.toObject())); + }); + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts new file mode 100644 index 0000000..8f658f3 --- /dev/null +++ b/src/routes/validate/user.ts @@ -0,0 +1,44 @@ +import joi from '@hapi/joi'; +import globals from "../../globals"; + +export default class UserValidate { // validate input for user + static input (data) { + return joi.object({ + name: joi.string() + .alphanum() + .lowercase() + .required(), + + email: joi.string() + .email({minDomainSegments: 2}) + .lowercase() + .required(), + + pass: joi.string() + .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#$%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')) + .required(), + + level: joi.string() + .valid(...globals.levels) + .required(), + + location: joi.string() + .alphanum() + .required(), + + device_name: joi.string() + .required() + }).validate(data); + } + + static output (data) { // validate output from database for needed properties, strip everything else + return joi.object({ + _id: joi.any(), + name: joi.string(), + email: joi.string(), + level: joi.string(), + location: joi.string(), + device_name: joi.string() + }).validate(data, {stripUnknown: true}).value; + } +} diff --git a/src/test/db.json b/src/test/db.json new file mode 100644 index 0000000..ba55d6f --- /dev/null +++ b/src/test/db.json @@ -0,0 +1,17 @@ +{ + "collections": { + "users": [ + { + "_id": "5ea0450ed851c30a90e70894", + "email": "jane.doe@bosch.com", + "name": "janedoe", + "pass": "$2a$10$KDKZjCsgDXwhtKdXZ9oG2ueDuCZsRKOMSqHuBfCM/2R0V6DRns.sy", + "level": "write", + "location": "Rng", + "device_name": "Alpha I", + "key": "5ea0450ed851c30a90e70899", + "__v": 0 + } + ] + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index c49a622..304952d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,13 +4,15 @@ "target": "es5", "outDir": "dist", "sourceMap": true, - "esModuleInterop": true + "esModuleInterop": true, + "resolveJsonModule": true }, "files": [ "./node_modules/@types/node/index.d.ts" ], "include": [ - "src/**/*.ts" + "src/**/*.ts", + "src/**/*.json" ], "exclude": [ "node_modules" From 90d34f1e1b27a1b35143247cef203850f28a41d7 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Wed, 22 Apr 2020 17:38:24 +0200 Subject: [PATCH 02/16] cannot add username twice --- oas/others.yaml | 2 +- src/db.ts | 4 +--- src/index.ts | 4 ++-- src/routes/root.spec.ts | 3 ++- src/routes/root.ts | 2 +- src/routes/user.spec.ts | 10 ++++++---- src/routes/user.ts | 21 +++++++++++++++------ 7 files changed, 28 insertions(+), 18 deletions(-) diff --git a/oas/others.yaml b/oas/others.yaml index df322fc..e5f200e 100644 --- a/oas/others.yaml +++ b/oas/others.yaml @@ -11,7 +11,7 @@ application/json: schema: properties: - message: + status: type: string example: 'API server up and running!' 500: diff --git a/src/db.ts b/src/db.ts index 1ac4222..98c8617 100644 --- a/src/db.ts +++ b/src/db.ts @@ -40,7 +40,7 @@ export default class db { }); mongoose.connection.on('error', console.error.bind(console, 'connection error:')); mongoose.connection.once('open', () => { - console.log(`Connected to ${connectionString}`); + console.log(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`); this.state.db = mongoose.connection; done(); }); @@ -60,7 +60,6 @@ export default class db { let dropCounter = 0; // count number of dropped collections to know when to return done() collections.forEach(collection => { // drop each collection this.state.db.dropCollection(collection.name, () => { - console.log('dropped collection ' + collection.name); if (++ dropCounter >= collections.length) { // all collections dropped done(); } @@ -79,7 +78,6 @@ export default class db { Object.keys(json.collections).forEach(collectionName => { // create each collection this.state.db.createCollection(collectionName, (err, collection) => { collection.insertMany(json.collections[collectionName], () => { // insert JSON data - console.log('loaded collection ' + collectionName); if (++ loadCounter >= Object.keys(json.collections).length) { // all collections loaded done(); } diff --git a/src/index.ts b/src/index.ts index 8c4af39..cfaf696 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import db from './db'; // tell if server is running in debug or production environment -console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : '===== DEVELOPMENT ====='); +console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT ====='); // mongodb connection @@ -53,7 +53,7 @@ app.use((err, req, res, ignore) => { // internal server error handling // hook up server to port const server = app.listen(port, () => { - console.log(`Listening on http://localhost:${port}`); + console.log(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`); }); module.exports = server; \ No newline at end of file diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index 0e2d625..276f159 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -8,6 +8,7 @@ describe('/', () => { before(done => { process.env.port = '2999'; + process.env.NODE_ENV = 'test'; db.connect('test', done); }); beforeEach(done => { @@ -26,7 +27,7 @@ describe('/', () => { .get('/') .expect('Content-type', /json/) .expect(200, (err, res) => { - should(res.body).be.eql({message: 'API server up and running!'}); + should(res.body).be.eql({status: 'API server up and running!'}); done(); }); }); diff --git a/src/routes/root.ts b/src/routes/root.ts index 896f360..bcbb40b 100644 --- a/src/routes/root.ts +++ b/src/routes/root.ts @@ -3,7 +3,7 @@ import express from 'express'; const router = express.Router(); router.get('/', (req, res) => { - res.json({message: 'API server up and running!'}); + res.json({status: 'API server up and running!'}); }); module.exports = router; diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index fe5c031..4c50a70 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -1,7 +1,7 @@ import supertest from 'supertest'; import should from 'should/as-function'; import db from '../db'; -import userModel from '../models/user'; +import UserModel from '../models/user'; describe('/user/new', () => { @@ -9,6 +9,7 @@ describe('/user/new', () => { before(done => { process.env.port = '2999'; + process.env.NODE_ENV = 'test'; db.connect('test', done); }); beforeEach(done => { @@ -44,7 +45,7 @@ describe('/user/new', () => { .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) .expect(200, err => { if (err) return done(err); - userModel.find({name: 'johndoe'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'johndoe'}).lean().exec( 'find', (err, data) => { if (err) return done(err); should(data).have.lengthOf(1); should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); @@ -63,9 +64,10 @@ describe('/user/new', () => { supertest(server) .post('/user/new') .send({email: 'j.doe@bosch.com', name: 'janedoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) - .expect(400, err => { + .expect(400, (err, res) => { if (err) return done(err); - userModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + should(res.body).be.eql({status: 'Username already taken'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { if (err) return done(err); should(data).have.lengthOf(1); done(); diff --git a/src/routes/user.ts b/src/routes/user.ts index ea2994f..4c1d8ed 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -18,12 +18,21 @@ router.post('/user/new', (req, res, next) => { return; } - user.key = mongoose.Types.ObjectId(); // use object id as unique API key - bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing - user.pass = hash; - new UserModel(user).save((err, data) => { // store user - if (err) next(err); - res.json(UserValidate.output(data.toObject())); + // check that user does not already exist + UserModel.find({name: user.name}).lean().exec( 'find', (err, data) => { + if (err) next(err); + if (data.length > 0) { + res.status(400).json({status: 'Username already taken'}); + return; + } + + user.key = mongoose.Types.ObjectId(); // use object id as unique API key + bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing + user.pass = hash; + new UserModel(user).save((err, data) => { // store user + if (err) next(err); + res.json(UserValidate.output(data.toObject())); + }); }); }); }); From 1a3fdc567de3fae13e74358d3ee78ce6e6dcbfb1 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Thu, 23 Apr 2020 13:59:45 +0200 Subject: [PATCH 03/16] added authorization --- oas/condition.yaml | 10 ++-- oas/material.yaml | 10 ++-- oas/measurement.yaml | 10 ++-- oas/model.yaml | 8 +-- oas/oas.yaml | 5 +- oas/others.yaml | 25 +++++++++ oas/sample.yaml | 14 +++-- oas/template.yaml | 24 ++++++--- oas/user.yaml | 17 +++--- package-lock.json | 18 +++++++ package.json | 4 ++ src/db.ts | 11 ++++ src/globals.ts | 2 +- src/helpers/authorize.ts | 100 ++++++++++++++++++++++++++++++++++++ src/index.ts | 16 ++++++ src/routes/root.spec.ts | 96 ++++++++++++++++++++++++++++++++-- src/routes/root.ts | 6 +++ src/routes/user.spec.ts | 45 +++++++++++++--- src/routes/user.ts | 3 ++ src/routes/validate/user.ts | 3 +- src/test/db.json | 13 ++++- 21 files changed, 393 insertions(+), 47 deletions(-) create mode 100644 src/helpers/authorize.ts diff --git a/oas/condition.yaml b/oas/condition.yaml index 1259ec1..cca8ca6 100644 --- a/oas/condition.yaml +++ b/oas/condition.yaml @@ -3,7 +3,7 @@ - $ref: 'oas.yaml#/components/parameters/Id' get: summary: TODO condition by id - description: 'levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, maintain, dev, admin' tags: - /condition responses: @@ -23,9 +23,11 @@ $ref: 'oas.yaml#/components/responses/500' put: summary: TODO add/change condition - description: 'levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /condition + security: + - BasicAuth: [] requestBody: required: true content: @@ -51,9 +53,11 @@ $ref: 'oas.yaml#/components/responses/500' delete: summary: TODO delete condition - description: 'levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /condition + security: + - BasicAuth: [] responses: 200: $ref: 'oas.yaml#/components/responses/Ok' diff --git a/oas/material.yaml b/oas/material.yaml index 2ba26d7..d5d7d34 100644 --- a/oas/material.yaml +++ b/oas/material.yaml @@ -3,7 +3,7 @@ - $ref: 'oas.yaml#/components/parameters/Id' get: summary: TODO get material details - description: 'levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, maintain, dev, admin' tags: - /material responses: @@ -21,9 +21,11 @@ $ref: 'oas.yaml#/components/responses/500' put: summary: TODO add/change material - description: 'levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /material + security: + - BasicAuth: [] requestBody: required: true content: @@ -47,9 +49,11 @@ $ref: 'oas.yaml#/components/responses/500' delete: summary: TODO delete material - description: 'levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /material + security: + - BasicAuth: [] responses: 200: $ref: 'oas.yaml#/components/responses/Ok' diff --git a/oas/measurement.yaml b/oas/measurement.yaml index 52c0430..0b4d5b2 100644 --- a/oas/measurement.yaml +++ b/oas/measurement.yaml @@ -3,7 +3,7 @@ - $ref: 'oas.yaml#/components/parameters/Id' get: summary: TODO measurement values by id - description: 'levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, maintain, dev, admin' tags: - /measurement responses: @@ -23,9 +23,11 @@ $ref: 'oas.yaml#/components/responses/500' put: summary: TODO add/change measurement - description: 'levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /measurement + security: + - BasicAuth: [] requestBody: required: true content: @@ -51,9 +53,11 @@ $ref: 'oas.yaml#/components/responses/500' delete: summary: TODO delete measurement - description: 'levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /measurement + security: + - BasicAuth: [] responses: 200: $ref: 'oas.yaml#/components/responses/Ok' diff --git a/oas/model.yaml b/oas/model.yaml index ce237e2..24df9af 100644 --- a/oas/model.yaml +++ b/oas/model.yaml @@ -3,7 +3,7 @@ - $ref: 'oas.yaml#/components/parameters/Name' get: summary: TODO get model data by name - description: 'levels: dev, admin' + description: 'Auth: all, levels: dev, admin' tags: - /model responses: @@ -24,7 +24,7 @@ $ref: 'oas.yaml#/components/responses/500' put: summary: TODO add/replace model data by name - description: 'levels: dev, admin' + description: 'Auth: all, levels: dev, admin' tags: - /model requestBody: @@ -50,9 +50,11 @@ $ref: 'oas.yaml#/components/responses/500' delete: summary: TODO delete model data - description: 'levels: dev, admin' + description: 'Auth: basic, levels: dev, admin' tags: - /model + security: + - BasicAuth: [] responses: 200: $ref: 'oas.yaml#/components/responses/Ok' diff --git a/oas/oas.yaml b/oas/oas.yaml index ba1bafd..03549c1 100644 --- a/oas/oas.yaml +++ b/oas/oas.yaml @@ -6,7 +6,10 @@ info: version: 1.0.0 description: | This API gives access to the project database.
    - Access is restricted. Authentication can be obtained with HTTP Basic Auth using username and password. Data access methods can also be accessed using an API key at the URL ending like ?key=xxx
    + Access is restricted. Authentication can be obtained with HTTP Basic Auth using username and password. + Data access methods can also be accessed using an API key at the URL ending like ?key=xxx
    + The description lists available authentication methods, also the locks of each method close correspondingly + if the entered authentication is allowed.

    There are a number of different user levels:
    • read: read access to the samples database
    • diff --git a/oas/others.yaml b/oas/others.yaml index e5f200e..c543797 100644 --- a/oas/others.yaml +++ b/oas/others.yaml @@ -1,6 +1,7 @@ /: get: summary: Root method + description: 'Auth: none' tags: - / security: [] @@ -14,5 +15,29 @@ status: type: string example: 'API server up and running!' + 500: + $ref: 'oas.yaml#/components/responses/500' + +/authorized: + get: + summary: Checks authorization + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - / + responses: + 200: + description: Authorized + content: + application/json: + schema: + properties: + status: + type: string + example: 'Authorization successful' + method: + type: string + example: 'basic' + 401: + $ref: 'oas.yaml#/components/responses/401' 500: $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/sample.yaml b/oas/sample.yaml index 8464e06..b84be19 100644 --- a/oas/sample.yaml +++ b/oas/sample.yaml @@ -1,7 +1,7 @@ /samples: get: summary: TODO all samples in overview - description: 'levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, maintain, dev, admin' tags: - /sample responses: @@ -20,7 +20,7 @@ - $ref: 'oas.yaml#/components/parameters/Id' get: summary: TODO sample details - description: 'levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, maintain, dev, admin' tags: - /sample responses: @@ -40,9 +40,11 @@ $ref: 'oas.yaml#/components/responses/500' put: summary: TODO add/change sample - description: 'levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /sample + security: + - BasicAuth: [] requestBody: required: true content: @@ -68,9 +70,11 @@ $ref: 'oas.yaml#/components/responses/500' delete: summary: TODO delete sample - description: 'levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /sample + security: + - BasicAuth: [] responses: 200: $ref: 'oas.yaml#/components/responses/Ok' @@ -87,7 +91,7 @@ /sample/notes/fields: get: summary: TODO list all existing field names for custom notes fields - description: 'levels: write, maintain, dev, admin' + description: 'Auth: all, levels: write, maintain, dev, admin' tags: - /sample responses: diff --git a/oas/template.yaml b/oas/template.yaml index bce58d0..a09cb21 100644 --- a/oas/template.yaml +++ b/oas/template.yaml @@ -1,7 +1,7 @@ /template/treatments: get: summary: TODO all available treatment methods - description: 'levels: read, write, maintain, dev, admin' + description: 'Auth: basic, levels: read, write, maintain, dev, admin' tags: - /templates security: @@ -30,7 +30,7 @@ - $ref: 'oas.yaml#/components/parameters/Name' get: summary: TODO treatment method details - description: 'levels: read, write, maintain, admin' + description: 'Auth: basic, levels: read, write, maintain, admin' tags: - /templates security: @@ -59,9 +59,11 @@ $ref: 'oas.yaml#/components/responses/500' put: summary: TODO add/change treatment method - description: 'levels: maintain, admin' + description: 'Auth: basic, levels: maintain, admin' tags: - /templates + security: + - BasicAuth: [] requestBody: required: true content: @@ -101,9 +103,11 @@ $ref: 'oas.yaml#/components/responses/500' delete: summary: TODO delete treatment method - description: 'levels: maintain, admin' + description: 'Auth: basic, levels: maintain, admin' tags: - /templates + security: + - BasicAuth: [] responses: 200: $ref: 'oas.yaml#/components/responses/Ok' @@ -120,7 +124,7 @@ /template/measurements: get: summary: TODO all available measurement methods - description: 'levels: read, write, maintain, dev, admin' + description: 'Auth: basic, levels: read, write, maintain, dev, admin' tags: - /templates security: @@ -150,7 +154,7 @@ - $ref: 'oas.yaml#/components/parameters/Name' get: summary: TODO measurement method details - description: 'levels: read, write, maintain, admin' + description: 'Auth: basic, levels: read, write, maintain, admin' tags: - /templates security: @@ -180,9 +184,11 @@ $ref: 'oas.yaml#/components/responses/500' put: summary: TODO add/change measurement method - description: 'levels: maintain, admin' + description: 'Auth: basic, levels: maintain, admin' tags: - /templates + security: + - BasicAuth: [] requestBody: required: true content: @@ -224,9 +230,11 @@ $ref: 'oas.yaml#/components/responses/500' delete: summary: TODO delete measurement method - description: 'levels: maintain, admin' + description: 'Auth: basic, levels: maintain, admin' tags: - /templates + security: + - BasicAuth: [] responses: 200: $ref: 'oas.yaml#/components/responses/Ok' diff --git a/oas/user.yaml b/oas/user.yaml index f8434f5..3db2b3c 100644 --- a/oas/user.yaml +++ b/oas/user.yaml @@ -1,7 +1,7 @@ /users: get: summary: TODO lists all users - description: 'levels: admin' + description: 'Auth: basic, levels: admin' tags: - /user security: @@ -26,7 +26,7 @@ - $ref: 'oas.yaml#/components/parameters/Name' get: summary: TODO list user details - description: 'levels: read, write, maintain, dev get their own information without a name property specified, level: admin can get any user using the name parameter' + description: 'Auth: basic, levels: read, write, maintain, dev get their own information without a name property specified, level: admin can get any user using the name parameter' tags: - /user security: @@ -52,9 +52,11 @@ $ref: 'oas.yaml#/components/responses/500' put: summary: TODO change user details - description: 'levels: read, write, maintain, dev can change their own information (except level) without a name property specified, level: admin can change any user using the name parameter' + description: 'Auth: basic, levels: read, write, maintain, dev can change their own information (except level) without a name property specified, level: admin can change any user using the name parameter' tags: - /user + security: + - BasicAuth: [] requestBody: required: true content: @@ -82,9 +84,11 @@ $ref: 'oas.yaml#/components/responses/500' delete: summary: TODO delete user - description: 'levels: read, write, maintain, dev can delete their own account, level: admin can delete any user using the name parameter' + description: 'Auth: basic, levels: read, write, maintain, dev can delete their own account, level: admin can delete any user using the name parameter' tags: - /user + security: + - BasicAuth: [] responses: 200: $ref: 'oas.yaml#/components/responses/Ok' @@ -101,7 +105,7 @@ /user/key: get: summary: TODO get API key for the user - description: 'levels: read, write, maintain, dev, admin' + description: 'Auth: basic, levels: read, write, maintain, dev, admin' tags: - /user security: @@ -120,7 +124,7 @@ /user/new: post: summary: TODO add new user - description: 'levels: admin' + description: 'Auth: basic, levels: admin' tags: - /user security: @@ -157,6 +161,7 @@ /user/passreset: post: summary: TODO reset password and send mail to restore + description: 'Auth: none' tags: - /user security: [] diff --git a/package-lock.json b/package-lock.json index 956d6f9..795376c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -208,6 +208,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, "bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -562,6 +570,11 @@ "safe-buffer": "5.1.2" } }, + "content-filter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/content-filter/-/content-filter-1.1.2.tgz", + "integrity": "sha512-VaZ4Y7h776r0v2WxWqu3iatjYI6/N0msXK8O1ymtkFWbSvaFoCePksS8U60BS6dUMZeAlqhN09SuM7ghdzRP1Q==" + }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -1440,6 +1453,11 @@ } } }, + "mongo-sanitize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mongo-sanitize/-/mongo-sanitize-1.1.0.tgz", + "integrity": "sha512-6gB9AiJD+om2eZLxaPKIP5Q8P3Fr+s+17rVWso7hU0+MAzmIvIMlgTYuyvalDLTtE/p0gczcvJ8A3pbN1XmQ/A==" + }, "mongodb": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.4.1.tgz", diff --git a/package.json b/package.json index 234bb65..7630a0c 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "API for the digital fingerprint of plastics mongodb", "main": "index.js", "scripts": { + "tsc": "tsc", "test": "mocha dist/**/**.spec.js", "start": "tsc && node dist/index.js", "dev": "nodemon -e ts,yaml --exec \"npm run start\"" @@ -16,11 +17,14 @@ "@hapi/joi": "^17.1.1", "@types/mocha": "^5.2.7", "@types/node": "^13.1.6", + "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", "body-parser": "^1.19.0", "cfenv": "^1.2.2", + "content-filter": "^1.1.2", "express": "^4.17.1", "json-schema": "^0.2.5", + "mongo-sanitize": "^1.1.0", "mongoose": "^5.8.7", "nodemon": "^2.0.3", "swagger-ui-express": "^4.1.2", diff --git a/src/db.ts b/src/db.ts index 98c8617..de45a74 100644 --- a/src/db.ts +++ b/src/db.ts @@ -39,6 +39,17 @@ export default class db { if (err) done(err); }); mongoose.connection.on('error', console.error.bind(console, 'connection error:')); + mongoose.connection.on('disconnected', () => { // reset state on disconnect + console.log('Database disconnected'); + this.state.db = 0; + done(); + }); + process.on('SIGINT', () => { // close connection when app is terminated + mongoose.connection.close(() => { + console.log('Mongoose default connection disconnected through app termination'); + process.exit(0); + }); + }); mongoose.connection.once('open', () => { console.log(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`); this.state.db = mongoose.connection; diff --git a/src/globals.ts b/src/globals.ts index e6db442..0d4ccdb 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -1,5 +1,5 @@ const globals = { - levels: [ + levels: [ // access levels 'read', 'write', 'maintain', diff --git a/src/helpers/authorize.ts b/src/helpers/authorize.ts new file mode 100644 index 0000000..e42f388 --- /dev/null +++ b/src/helpers/authorize.ts @@ -0,0 +1,100 @@ +import basicAuth from 'basic-auth'; +import bcrypt from 'bcryptjs'; +import UserModel from '../models/user'; + + +// appends req.auth(res, ['levels'], method = 'all') +// which returns sends error message and returns false if unauthorized, otherwise true +// req.authDetails returns eg. {methods: ['basic'], username: 'johndoe', level: 'write'} + +module.exports = async (req, res, next) => { + let givenMethod = ''; // authorization method given by client, basic taken preferred + let user = {name: '', level: ''}; // user object + + // test authentications + const userBasic = await basic(req, next); + + if (userBasic) { // basic available + givenMethod = 'basic'; + user = userBasic; + } + else { // if basic not available, test key + const userKey = await key(req, next); + if (userKey) { + givenMethod = 'key'; + user = userKey; + } + } + + req.auth = (res, levels, method = 'all') => { + if (givenMethod === method || (method === 'all' && givenMethod !== '')) { // method is available + if (levels.indexOf(user.level) > -1) { // level is available + return true; + } + else { + res.status(403).json({status: 'Forbidden'}); + return false; + } + } + else { + res.status(401).json({status: 'Unauthorized'}); + return false; + } + } + + req.authDetails = { + method: givenMethod, + username: user.name, + level: user.level + }; + + next(); +} + + +function basic (req, next): any { // checks basic auth and returns changed user object + return new Promise(resolve => { + const auth = basicAuth(req); + if (auth !== undefined) { // basic auth available + UserModel.find({name: auth.name}).lean().exec( 'find', (err, data) => { // find user + if (err) next(err); + if (data.length === 1) { // one user found + bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password + if (err) next(err); + if (res === true) { + resolve({level: data[0].level, name: data[0].name}); + } + else { + resolve(null); + } + }); + } + else { + resolve(null); + } + }); + } + else { + resolve(null); + } + }); +} + +function key (req, next): any { // checks API key and returns changed user object + return new Promise(resolve => { + if (req.query.key !== undefined) { + UserModel.find({key: req.query.key}).lean().exec( 'find', (err, data) => { // find user + if (err) next(err); + if (data.length === 1) { // one user found + resolve({level: data[0].level, name: data[0].name}); + } + else { + resolve(null); + } + }); + } + else { + resolve(null); + } + }); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index cfaf696..67e29e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,8 @@ import express from 'express'; import bodyParser from 'body-parser'; import swagger from 'swagger-ui-express'; import jsonRefParser, {JSONSchema} from '@apidevtools/json-schema-ref-parser'; +import contentFilter from 'content-filter'; +import mongoSanitize from 'mongo-sanitize'; import db from './db'; @@ -23,9 +25,23 @@ const port = process.env.PORT || 3000; app.use(express.json({ limit: '5mb'})); app.use(express.urlencoded({ extended: false, limit: '5mb' })); app.use(bodyParser.json()); +app.use(contentFilter()); // filter URL query attacks +app.use((req, res, next) => { // filter body query attacks + req.body = mongoSanitize(req.body); + next(); +}); app.use((err, req, res, ignore) => { // bodyParser error handling res.status(400).send({status: 'Invalid JSON body'}); }); +app.use((req, res, next) => { // no database connection error + if (db.getState().db) { + next(); + } + else { + res.status(500).send({status: 'Internal server error'}); + } +}); +app.use(require('./helpers/authorize')); // handle authentication // require routes app.use('/', require('./routes/root')); diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index 276f159..61544a8 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -26,14 +26,16 @@ describe('/', () => { supertest(server) .get('/') .expect('Content-type', /json/) - .expect(200, (err, res) => { + .expect(200) + .end((err, res) => { + if (err) done (err); should(res.body).be.eql({status: 'API server up and running!'}); done(); }); }); }); -describe('Testing unknown routes', () => { +describe('Unknown routes', () => { let server; before(done => { @@ -50,10 +52,94 @@ describe('Testing unknown routes', () => { afterEach(done => { server.close(done); }); - it('returns a 404 message', done => { + it('return a 404 message', done => { supertest(server) .get('/unknownroute') - .expect(404); - done(); + .expect(404) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Not found'}); + done(); + }); + }); +}); + +describe('An unauthorized request', () => { + let server; + + before(done => { + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns a 401 message', done => { + supertest(server) + .get('/authorized') + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); + it('does not work with correct username', done => { + supertest(server) + .get('/authorized') + .auth('admin', 'Abc123!!') + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); +}); + +describe('An authorized request', () => { + let server; + + before(done => { + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('works with an API key', done => { + supertest(server) + .get('/authorized?key=5ea131671feb9c2ee0aafc9a') + .expect(200) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Authorization successful', method: 'key'}); + done(); + }); + }); + it('works with basic auth', done => { + supertest(server) + .get('/authorized') + .auth('admin', 'Abc123!#') + .expect(200) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Authorization successful', method: 'basic'}); + done(); + }); }); }); \ No newline at end of file diff --git a/src/routes/root.ts b/src/routes/root.ts index bcbb40b..2705280 100644 --- a/src/routes/root.ts +++ b/src/routes/root.ts @@ -1,4 +1,5 @@ import express from 'express'; +import globals from '../globals'; const router = express.Router(); @@ -6,4 +7,9 @@ router.get('/', (req, res) => { res.json({status: 'API server up and running!'}); }); +router.get('/authorized', (req, res) => { + if (!req.auth(res, globals.levels)) return; + res.json({status: 'Authorization successful', method: req.authDetails.method}); +}); + module.exports = router; diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index 4c50a70..f78d387 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -26,10 +26,12 @@ describe('/user/new', () => { it('returns the added user data', done => { supertest(server) .post('/user/new') + .auth('admin', 'Abc123!#') .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) .expect('Content-type', /json/) - .expect(200, (err, res) => { - if (err) return done(err); + .expect(200) + .end((err, res) => { + if (err) done (err); should(res.body).have.property('_id').be.type('string'); should(res.body).have.property('email', 'john.doe@bosch.com'); should(res.body).have.property('name', 'johndoe'); @@ -42,9 +44,11 @@ describe('/user/new', () => { it('stores the data', done => { supertest(server) .post('/user/new') + .auth('admin', 'Abc123!#') .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) - .expect(200, err => { - if (err) return done(err); + .expect(200) + .end(err => { + if (err) done (err); UserModel.find({name: 'johndoe'}).lean().exec( 'find', (err, data) => { if (err) return done(err); should(data).have.lengthOf(1); @@ -63,9 +67,11 @@ describe('/user/new', () => { it('rejects a username already in use', done => { supertest(server) .post('/user/new') + .auth('admin', 'Abc123!#') .send({email: 'j.doe@bosch.com', name: 'janedoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) - .expect(400, (err, res) => { - if (err) return done(err); + .expect(400) + .end((err, res) => { + if (err) done (err); should(res.body).be.eql({status: 'Username already taken'}); UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { if (err) return done(err); @@ -73,5 +79,30 @@ describe('/user/new', () => { done(); }); }); - }); // TODO: authentication + }); + it('rejects requests from non-admins', done => { + supertest(server) + .post('/user/new') + .auth('janedoe', 'Abc123!#') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) + .expect('Content-type', /json/) + .expect(403) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Forbidden'}); + done(); + }); + }); + it('rejects requests from an admin API key', done => { + supertest(server) + .post('/user/new?key=5ea131671feb9c2ee0aafc9a') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); }); \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts index 4c1d8ed..ffaebc7 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -11,6 +11,9 @@ router.get('/users', (req, res) => { }); router.post('/user/new', (req, res, next) => { + console.log(req.authDetails); + if (!req.auth(res, ['admin'], 'basic')) return; + // validate input const {error, value: user} = UserValidate.input(req.body); if(error !== undefined) { diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index 8f658f3..1cccf41 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -1,5 +1,5 @@ import joi from '@hapi/joi'; -import globals from "../../globals"; +import globals from '../../globals'; export default class UserValidate { // validate input for user static input (data) { @@ -27,6 +27,7 @@ export default class UserValidate { // validate input for user .required(), device_name: joi.string() + .allow('') .required() }).validate(data); } diff --git a/src/test/db.json b/src/test/db.json index ba55d6f..7e32395 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -5,12 +5,23 @@ "_id": "5ea0450ed851c30a90e70894", "email": "jane.doe@bosch.com", "name": "janedoe", - "pass": "$2a$10$KDKZjCsgDXwhtKdXZ9oG2ueDuCZsRKOMSqHuBfCM/2R0V6DRns.sy", + "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K", "level": "write", "location": "Rng", "device_name": "Alpha I", "key": "5ea0450ed851c30a90e70899", "__v": 0 + }, + { + "_id": "5ea131671feb9c2ee0aafc9b", + "email": "a.d.m.i.n@bosch.com", + "name": "admin", + "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K", + "level": "admin", + "location": "Rng", + "device_name": "", + "key": "5ea131671feb9c2ee0aafc9a", + "__v": "0" } ] } From 4e68267bfd361dd5c39d30bd87d56f94c50828c5 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Thu, 23 Apr 2020 17:46:00 +0200 Subject: [PATCH 04/16] added passreset and mail helper --- oas/schemas.yaml | 11 +++-- oas/user.yaml | 8 ++-- package-lock.json | 26 ++++++++++++ package.json | 1 + src/db.ts | 1 + src/helpers/mail.ts | 64 ++++++++++++++++++++++++++++ src/routes/user.spec.ts | 92 +++++++++++++++++++++++++++++++++++++++++ src/routes/user.ts | 25 ++++++++++- 8 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 src/helpers/mail.ts diff --git a/oas/schemas.yaml b/oas/schemas.yaml index 82c9508..d4151d1 100644 --- a/oas/schemas.yaml +++ b/oas/schemas.yaml @@ -141,14 +141,17 @@ Email: email: type: string example: john.doe@bosch.com -User: - allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - - $ref: 'oas.yaml#/components/schemas/Email' +UserName: properties: name: type: string example: johndoe +User: + allOf: + - $ref: 'oas.yaml#/components/schemas/_Id' + - $ref: 'oas.yaml#/components/schemas/UserName' + - $ref: 'oas.yaml#/components/schemas/Email' + properties: pass: type: string writeOnly: true diff --git a/oas/user.yaml b/oas/user.yaml index 3db2b3c..4042901 100644 --- a/oas/user.yaml +++ b/oas/user.yaml @@ -171,11 +171,13 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/Email' + allOf: + - $ref: 'oas.yaml#/components/schemas/UserName' + - $ref: 'oas.yaml#/components/schemas/Email' responses: 200: $ref: 'oas.yaml#/components/responses/Ok' - 401: - $ref: 'oas.yaml#/components/responses/401' + 404: + $ref: 'oas.yaml#/components/responses/404' 500: $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 795376c..b8354bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -203,6 +203,14 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -848,6 +856,24 @@ "is-buffer": "~2.0.3" } }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "form-data": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", diff --git a/package.json b/package.json index 7630a0c..d5a2bfe 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@hapi/joi": "^17.1.1", "@types/mocha": "^5.2.7", "@types/node": "^13.1.6", + "axios": "^0.19.2", "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", "body-parser": "^1.19.0", diff --git a/src/db.ts b/src/db.ts index de45a74..b93fb6f 100644 --- a/src/db.ts +++ b/src/db.ts @@ -51,6 +51,7 @@ export default class db { }); }); mongoose.connection.once('open', () => { + mongoose.set('useFindAndModify', false); console.log(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`); this.state.db = mongoose.connection; done(); diff --git a/src/helpers/mail.ts b/src/helpers/mail.ts new file mode 100644 index 0000000..949d243 --- /dev/null +++ b/src/helpers/mail.ts @@ -0,0 +1,64 @@ +import axios from 'axios'; + +// sends an email + +export default (mailAddress, subject, content, f) => { // callback, executed empty or with error + if (process.env.NODE_ENV === 'production') { + const mailService = JSON.parse(process.env.VCAP_SERVICES).Mail[0]; + axios({ + method: 'post', + url: mailService.credentials.uri + '/email', + auth: {username: mailService.credentials.username, password: mailService.credentials.password}, + data: { + recipients: [{to: mailAddress}], + subject: {content: subject}, + body: { + content: content, + contentType: "text/html" + }, + from: { + eMail: "dfop@bosch-iot.com", + password: "PlasticsOfFingerprintDigital" + } + } + }) + .then(() => { + f(); + }) + .catch((err) => { + f(err); + }); + } + else if (process.env.NODE_ENV === 'test') { + console.log('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content); + f(); + } + else { // dev + axios({ + method: 'get', + url: 'https://digital-fingerprint-of-plastics-mail-test.apps.de1.bosch-iot-cloud.com/api', + data: { + method: 'post', + url: '/email', + data: { + recipients: [{to: mailAddress}], + subject: {content: subject}, + body: { + content: content, + contentType: "text/html" + }, + from: { + eMail: "dfop-test@bosch-iot.com", + password: "PlasticsOfFingerprintDigital" + } + } + } + }) + .then(() => { + f(); + }) + .catch((err) => { + f(err); + }); + } +} \ No newline at end of file diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index f78d387..a6ebec0 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -105,4 +105,96 @@ describe('/user/new', () => { done(); }); }); +}); + + + + +describe('/user/passreset', () => { + let server; + + before(done => { + process.env.port = '2999'; + process.env.NODE_ENV = 'test'; + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns the ok response', done => { + supertest(server) + .post('/user/passreset') + .send({ + email: 'jane.doe@bosch.com', + name: 'janedoe' + }) + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done(err); + should(res.body).be.eql({status: 'OK'}); + done(); + }); + }); + it('returns 404 for wrong username/email combo', done => { + supertest(server) + .post('/user/passreset') + .send({ + email: 'jane.doe@bosch.com', + name: 'admin' + }) + .expect('Content-type', /json/) + .expect(404) + .end((err, res) => { + if (err) done(err); + should(res.body).be.eql({status: 'Not found'}); + done(); + }); + }); + it('returns 404 for unknown username', done => { + supertest(server) + .post('/user/passreset') + .send({ + email: 'jane.doe@bosch.com', + name: 'admin' + }) + .expect('Content-type', /json/) + .expect(404) + .end((err, res) => { + if (err) done(err); + should(res.body).be.eql({status: 'Not found'}); + done(); + }); + }); + it('changes the user password', done => { + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + const oldpass = data[0].pass; + supertest(server) + .post('/user/passreset') + .send({ + email: 'jane.doe@bosch.com', + name: 'janedoe' + }) + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done(err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data[0].pass).not.eql(oldpass); + done(); + }); + }); + }); + }); }); \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts index ffaebc7..e4a17d5 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -3,6 +3,7 @@ import mongoose from 'mongoose'; import bcrypt from 'bcryptjs'; import UserValidate from './validate/user'; import UserModel from '../models/user'; +import mail from '../helpers/mail'; const router = express.Router(); @@ -11,7 +12,6 @@ router.get('/users', (req, res) => { }); router.post('/user/new', (req, res, next) => { - console.log(req.authDetails); if (!req.auth(res, ['admin'], 'basic')) return; // validate input @@ -40,4 +40,27 @@ router.post('/user/new', (req, res, next) => { }); }); +router.post('/user/passreset', (req, res, next) => { + // check if user/email combo exists + UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( 'find', (err, data) => { + if (err) next(err); + if (data.length === 1) { // it exists + const newPass = Math.random().toString(36).substring(2); + bcrypt.hash(newPass, 10, (err, hash) => { // password hashing + if (err) next(err); + UserModel.findOneAndUpdate({name: req.body.name, email: req.body.email}, {pass: hash}, err => { + if (err) next(err); + mail(data[0].email, 'Your new password for the DFOP database', 'Hi,

      You requested to reset your password.
      Your new password is:

      ' + newPass + '

      If you did not request a password reset, talk to the sysadmin quickly!

      Have a nice day.

      The DFOP team', err => { + if (err) next(err); + res.json({status: 'OK'}); + }); + }); + }); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + module.exports = router; \ No newline at end of file From 8bf408138f73cbae64eef5b6028a76376a35a20d Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Fri, 24 Apr 2020 10:53:45 +0200 Subject: [PATCH 05/16] changed to findById and improved db.loadJson --- package-lock.json | 31 +++++++++++++++++++++++++++++++ package.json | 2 ++ src/db.ts | 6 ++++++ src/helpers/authorize.ts | 4 ++-- src/routes/user.spec.ts | 5 +++-- src/routes/user.ts | 4 ++-- src/test/db.json | 4 ++-- 7 files changed, 48 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index b8354bc..5249707 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,19 @@ "defer-to-connect": "^1.0.1" } }, + "@types/bcrypt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-3.0.0.tgz", + "integrity": "sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ==" + }, + "@types/bson": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.2.tgz", + "integrity": "sha512-+uWmsejEHfmSjyyM/LkrP0orfE2m5Mx9Xel4tXNeqi1ldK5XMQcDsFkBmLDtuyKUbxj2jGDo0H240fbCRJZo7Q==", + "requires": { + "@types/node": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -103,6 +116,24 @@ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==" }, + "@types/mongodb": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.10.tgz", + "integrity": "sha512-6NkJNfFdFa/njBvN/9eAfq78bWUnapkdR3JbWGGpd7U71PjgKweA4Tlag8psi2mqm973vBYVTD1oc1u0lzRcig==", + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, + "@types/mongoose": { + "version": "5.7.12", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.7.12.tgz", + "integrity": "sha512-yzLJk3cdSwuMXaIacUCWUb8m960YcgnID7S4ZPOOgzT39aSC46670TuunN+ajDio7OUcGG4mGg8eOGs2Z6VmrA==", + "requires": { + "@types/mongodb": "*", + "@types/node": "*" + } + }, "@types/node": { "version": "13.1.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.6.tgz", diff --git a/package.json b/package.json index d5a2bfe..adc9874 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "dependencies": { "@apidevtools/json-schema-ref-parser": "^8.0.0", "@hapi/joi": "^17.1.1", + "@types/bcrypt": "^3.0.0", "@types/mocha": "^5.2.7", + "@types/mongoose": "^5.7.12", "@types/node": "^13.1.6", "axios": "^0.19.2", "basic-auth": "^2.0.1", diff --git a/src/db.ts b/src/db.ts index b93fb6f..00477da 100644 --- a/src/db.ts +++ b/src/db.ts @@ -88,6 +88,12 @@ export default class db { let loadCounter = 0; // count number of loaded collections to know when to return done() Object.keys(json.collections).forEach(collectionName => { // create each collection + for(let i in json.collections[collectionName]) { // convert $oid fields to actual ObjectIds + console.log(json.collections[collectionName][i]); + Object.keys(json.collections[collectionName][i]).forEach(key => { + json.collections[collectionName][i][key] = json.collections[collectionName][i][key].hasOwnProperty('$oid') ? mongoose.Types.ObjectId(json.collections[collectionName][i][key].$oid) : json.collections[collectionName][i][key]; + }) + } this.state.db.createCollection(collectionName, (err, collection) => { collection.insertMany(json.collections[collectionName], () => { // insert JSON data if (++ loadCounter >= Object.keys(json.collections).length) { // all collections loaded diff --git a/src/helpers/authorize.ts b/src/helpers/authorize.ts index e42f388..d3c7e75 100644 --- a/src/helpers/authorize.ts +++ b/src/helpers/authorize.ts @@ -56,7 +56,7 @@ function basic (req, next): any { // checks basic auth and returns changed user return new Promise(resolve => { const auth = basicAuth(req); if (auth !== undefined) { // basic auth available - UserModel.find({name: auth.name}).lean().exec( 'find', (err, data) => { // find user + UserModel.find({name: auth.name}).lean().exec( (err, data: any) => { // find user if (err) next(err); if (data.length === 1) { // one user found bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password @@ -83,7 +83,7 @@ function basic (req, next): any { // checks basic auth and returns changed user function key (req, next): any { // checks API key and returns changed user object return new Promise(resolve => { if (req.query.key !== undefined) { - UserModel.find({key: req.query.key}).lean().exec( 'find', (err, data) => { // find user + UserModel.find({key: req.query.key}).lean().exec( (err, data: any) => { // find user if (err) next(err); if (data.length === 1) { // one user found resolve({level: data[0].level, name: data[0].name}); diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index a6ebec0..c4511ec 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -175,7 +175,7 @@ describe('/user/passreset', () => { }); }); it('changes the user password', done => { - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data: any) => { if (err) return done(err); const oldpass = data[0].pass; supertest(server) @@ -189,8 +189,9 @@ describe('/user/passreset', () => { .end((err, res) => { if (err) done(err); should(res.body).be.eql({status: 'OK'}); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { if (err) return done(err); + console.log(data); should(data[0].pass).not.eql(oldpass); done(); }); diff --git a/src/routes/user.ts b/src/routes/user.ts index e4a17d5..cd67d14 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -42,13 +42,13 @@ router.post('/user/new', (req, res, next) => { router.post('/user/passreset', (req, res, next) => { // check if user/email combo exists - UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( 'find', (err, data) => { + UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( (err, data: any) => { if (err) next(err); if (data.length === 1) { // it exists const newPass = Math.random().toString(36).substring(2); bcrypt.hash(newPass, 10, (err, hash) => { // password hashing if (err) next(err); - UserModel.findOneAndUpdate({name: req.body.name, email: req.body.email}, {pass: hash}, err => { + UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}, err => { // write new password if (err) next(err); mail(data[0].email, 'Your new password for the DFOP database', 'Hi,

      You requested to reset your password.
      Your new password is:

      ' + newPass + '

      If you did not request a password reset, talk to the sysadmin quickly!

      Have a nice day.

      The DFOP team', err => { if (err) next(err); diff --git a/src/test/db.json b/src/test/db.json index 7e32395..af2d78f 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -2,7 +2,7 @@ "collections": { "users": [ { - "_id": "5ea0450ed851c30a90e70894", + "_id": {"$oid":"5ea0450ed851c30a90e70894"}, "email": "jane.doe@bosch.com", "name": "janedoe", "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K", @@ -13,7 +13,7 @@ "__v": 0 }, { - "_id": "5ea131671feb9c2ee0aafc9b", + "_id": {"$oid":"5ea131671feb9c2ee0aafc9b"}, "email": "a.d.m.i.n@bosch.com", "name": "admin", "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K", From a64229d1dc605360d489eedf77e413bce1160b90 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Fri, 24 Apr 2020 12:25:32 +0200 Subject: [PATCH 06/16] added GET /user route --- oas/schemas.yaml | 1 + oas/user.yaml | 117 +++++++++++++--- src/db.ts | 3 +- src/models/user.ts | 2 +- src/routes/root.spec.ts | 2 +- src/routes/user.spec.ts | 263 +++++++++++++++++++++++++++++++++++- src/routes/user.ts | 21 ++- src/routes/validate/user.ts | 7 +- src/test/db.json | 2 +- 9 files changed, 385 insertions(+), 33 deletions(-) diff --git a/oas/schemas.yaml b/oas/schemas.yaml index d4151d1..4d8a805 100644 --- a/oas/schemas.yaml +++ b/oas/schemas.yaml @@ -1,5 +1,6 @@ Id: type: string + example: 5ea0450ed851c30a90e70894 _Id: properties: _id: diff --git a/oas/user.yaml b/oas/user.yaml index 4042901..763a051 100644 --- a/oas/user.yaml +++ b/oas/user.yaml @@ -1,6 +1,6 @@ /users: get: - summary: TODO lists all users + summary: lists all users description: 'Auth: basic, levels: admin' tags: - /user @@ -21,12 +21,10 @@ $ref: 'oas.yaml#/components/responses/403' 500: $ref: 'oas.yaml#/components/responses/500' -/user/{name}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Name' +/user: get: - summary: TODO list user details - description: 'Auth: basic, levels: read, write, maintain, dev get their own information without a name property specified, level: admin can get any user using the name parameter' + summary: list own user details + description: 'Auth: basic, levels: read, write, maintain, admin' tags: - /user security: @@ -37,9 +35,7 @@ content: application/json: schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'oas.yaml#/components/schemas/User' 400: $ref: 'oas.yaml#/components/responses/400' 401: @@ -52,7 +48,98 @@ $ref: 'oas.yaml#/components/responses/500' put: summary: TODO change user details - description: 'Auth: basic, levels: read, write, maintain, dev can change their own information (except level) without a name property specified, level: admin can change any user using the name parameter' + description: 'Auth: basic, levels: read, write, maintain, admin' + tags: + - /user + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: 'oas.yaml#/components/schemas/_Id' + - $ref: 'oas.yaml#/components/schemas/UserName' + - $ref: 'oas.yaml#/components/schemas/Email' + properties: + pass: + type: string + writeOnly: true + example: Abc123!# + location: + type: string + example: Rng + device_name: + type: string + example: Alpha II + responses: + 200: + description: user details + content: + application/json: + schema: + $ref: 'oas.yaml#/components/schemas/User' + 400: + $ref: 'oas.yaml#/components/responses/400' + 401: + $ref: 'oas.yaml#/components/responses/401' + 403: + $ref: 'oas.yaml#/components/responses/403' + 404: + $ref: 'oas.yaml#/components/responses/404' + 500: + $ref: 'oas.yaml#/components/responses/500' + delete: + summary: TODO delete user + description: 'Auth: basic, levels: read, write, maintain, admin' + tags: + - /user + security: + - BasicAuth: [] + responses: + 200: + $ref: 'oas.yaml#/components/responses/Ok' + 400: + $ref: 'oas.yaml#/components/responses/400' + 401: + $ref: 'oas.yaml#/components/responses/401' + 403: + $ref: 'oas.yaml#/components/responses/403' + 404: + $ref: 'oas.yaml#/components/responses/404' + 500: + $ref: 'oas.yaml#/components/responses/500' +/user/{name}: + parameters: + - $ref: 'oas.yaml#/components/parameters/Name' + get: + summary: list user details + description: 'Auth: basic, levels: admin' + tags: + - /user + security: + - BasicAuth: [] + responses: + 200: + description: user details + content: + application/json: + schema: + $ref: 'oas.yaml#/components/schemas/User' + 400: + $ref: 'oas.yaml#/components/responses/400' + 401: + $ref: 'oas.yaml#/components/responses/401' + 403: + $ref: 'oas.yaml#/components/responses/403' + 404: + $ref: 'oas.yaml#/components/responses/404' + 500: + $ref: 'oas.yaml#/components/responses/500' + put: + summary: TODO change user details + description: 'Auth: basic, levels: admin' tags: - /user security: @@ -69,9 +156,7 @@ content: application/json: schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'oas.yaml#/components/schemas/User' 400: $ref: 'oas.yaml#/components/responses/400' 401: @@ -84,7 +169,7 @@ $ref: 'oas.yaml#/components/responses/500' delete: summary: TODO delete user - description: 'Auth: basic, levels: read, write, maintain, dev can delete their own account, level: admin can delete any user using the name parameter' + description: 'Auth: basic, levels: admin' tags: - /user security: @@ -123,7 +208,7 @@ $ref: 'oas.yaml#/components/responses/500' /user/new: post: - summary: TODO add new user + summary: add new user description: 'Auth: basic, levels: admin' tags: - /user @@ -160,7 +245,7 @@ $ref: 'oas.yaml#/components/responses/500' /user/passreset: post: - summary: TODO reset password and send mail to restore + summary: reset password and send mail to restore description: 'Auth: none' tags: - /user diff --git a/src/db.ts b/src/db.ts index 00477da..e624696 100644 --- a/src/db.ts +++ b/src/db.ts @@ -35,7 +35,7 @@ export default class db { } // connect to db - mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true}, err => { + mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true}, err => { if (err) done(err); }); mongoose.connection.on('error', console.error.bind(console, 'connection error:')); @@ -89,7 +89,6 @@ export default class db { let loadCounter = 0; // count number of loaded collections to know when to return done() Object.keys(json.collections).forEach(collectionName => { // create each collection for(let i in json.collections[collectionName]) { // convert $oid fields to actual ObjectIds - console.log(json.collections[collectionName][i]); Object.keys(json.collections[collectionName][i]).forEach(key => { json.collections[collectionName][i][key] = json.collections[collectionName][i][key].hasOwnProperty('$oid') ? mongoose.Types.ObjectId(json.collections[collectionName][i][key].$oid) : json.collections[collectionName][i][key]; }) diff --git a/src/models/user.ts b/src/models/user.ts index b72dabb..50178a6 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; const UserSchema = new mongoose.Schema({ - name: String, + name: {type: String, index: {unique: true}}, email: String, pass: String, key: String, diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index 61544a8..9372259 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -3,7 +3,7 @@ import should from 'should/as-function'; import db from '../db'; -describe('/', () => { +describe('GET /', () => { let server; before(done => { diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index c4511ec..93b598b 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -4,7 +4,258 @@ import db from '../db'; import UserModel from '../models/user'; -describe('/user/new', () => { +describe('GET /users', () => { + let server; + + before(done => { + process.env.port = '2999'; + process.env.NODE_ENV = 'test'; + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns all users', done => { + supertest(server) + .get('/users') + .auth('admin', 'Abc123!#') + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done (err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.users.length); + should(res.body).matchEach(user => { + should(user).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(user).have.property('_id').be.type('string'); + should(user).have.property('email').be.type('string'); + should(user).have.property('name').be.type('string'); + should(user).have.property('level').be.type('string'); + should(user).have.property('location').be.type('string'); + should(user).have.property('device_name').be.type('string'); + }); + done(); + }); + }); + it('rejects requests from non-admins', done => { + supertest(server) + .get('/users') + .auth('janedoe', 'Xyz890*)') + .expect('Content-type', /json/) + .expect(403) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Forbidden'}); + done(); + }); + }); + it('rejects requests from an admin API key', done => { + supertest(server) + .get('/users?key=5ea131671feb9c2ee0aafc9a') + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); +}); + + +describe('GET /user/{name}', () => { + let server; + + before(done => { + process.env.port = '2999'; + process.env.NODE_ENV = 'test'; + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns own user details', done => { + supertest(server) + .get('/user') + .auth('janedoe', 'Xyz890*)') + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('returns other user details for admin', done => { + supertest(server) + .get('/user/janedoe') + .auth('admin', 'Abc123!#') + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('rejects requests from non-admins for another user', done => { + supertest(server) + .get('/user/admin') + .auth('janedoe', 'Xyz890*)') + .expect('Content-type', /json/) + .expect(403) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Forbidden'}); + done(); + }); + }); + it('rejects requests from a user API key', done => { + supertest(server) + .get('/user?key=5ea0450ed851c30a90e70899') + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); + it('rejects requests from an admin API key', done => { + supertest(server) + .get('/user/janedoe?key=5ea131671feb9c2ee0aafc9a') + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); +}); + + +describe('PUT /user/{name}', () => { + let server; + + before(done => { + process.env.port = '2999'; + process.env.NODE_ENV = 'test'; + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns own user details', done => { + supertest(server) + .get('/user') + .auth('janedoe', 'Xyz890*)') + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('returns other user details for admin', done => { + supertest(server) + .get('/user/janedoe') + .auth('admin', 'Abc123!#') + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('rejects requests from non-admins for another user', done => { + supertest(server) + .get('/user/admin') + .auth('janedoe', 'Xyz890*)') + .expect('Content-type', /json/) + .expect(403) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Forbidden'}); + done(); + }); + }); + it('rejects requests from a user API key', done => { + supertest(server) + .get('/user?key=5ea0450ed851c30a90e70899') + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); + it('rejects requests from an admin API key', done => { + supertest(server) + .get('/user/janedoe?key=5ea131671feb9c2ee0aafc9a') + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); +}); + + +describe('POST /user/new', () => { let server; before(done => { @@ -32,6 +283,7 @@ describe('/user/new', () => { .expect(200) .end((err, res) => { if (err) done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); should(res.body).have.property('_id').be.type('string'); should(res.body).have.property('email', 'john.doe@bosch.com'); should(res.body).have.property('name', 'johndoe'); @@ -83,7 +335,7 @@ describe('/user/new', () => { it('rejects requests from non-admins', done => { supertest(server) .post('/user/new') - .auth('janedoe', 'Abc123!#') + .auth('janedoe', 'Xyz890*)') .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) .expect('Content-type', /json/) .expect(403) @@ -108,9 +360,7 @@ describe('/user/new', () => { }); - - -describe('/user/passreset', () => { +describe('POST /user/passreset', () => { let server; before(done => { @@ -191,11 +441,10 @@ describe('/user/passreset', () => { should(res.body).be.eql({status: 'OK'}); UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { if (err) return done(err); - console.log(data); should(data[0].pass).not.eql(oldpass); done(); }); }); }); }); -}); \ No newline at end of file +}); diff --git a/src/routes/user.ts b/src/routes/user.ts index cd67d14..0865fac 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -8,7 +8,24 @@ import mail from '../helpers/mail'; const router = express.Router(); router.get('/users', (req, res) => { - res.json({message: 'users up and running!'}); + if (!req.auth(res, ['admin'], 'basic')) return; + + UserModel.find({}).lean().exec( (err, data:any) => { + res.json(data.map(e => UserValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }); +}); + +router.get('/user/:username*?', (req, res) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + let username = req.authDetails.username; + if (req.params.username !== undefined) { + if (!req.auth(res, ['admin'], 'basic')) return; + username = req.params.username; + } + + UserModel.findOne({name: username}).lean().exec( (err, data:any) => { + res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + }); }); router.post('/user/new', (req, res, next) => { @@ -22,7 +39,7 @@ router.post('/user/new', (req, res, next) => { } // check that user does not already exist - UserModel.find({name: user.name}).lean().exec( 'find', (err, data) => { + UserModel.find({name: user.name}).lean().exec( (err, data:any) => { if (err) next(err); if (data.length > 0) { res.status(400).json({status: 'Username already taken'}); diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index 1cccf41..26becdd 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -33,13 +33,14 @@ export default class UserValidate { // validate input for user } static output (data) { // validate output from database for needed properties, strip everything else - return joi.object({ + const {value, error} = joi.object({ _id: joi.any(), name: joi.string(), email: joi.string(), level: joi.string(), location: joi.string(), - device_name: joi.string() - }).validate(data, {stripUnknown: true}).value; + device_name: joi.string().allow('') + }).validate(data, {stripUnknown: true}) + return error !== undefined? null : value; } } diff --git a/src/test/db.json b/src/test/db.json index af2d78f..4c2c645 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -5,7 +5,7 @@ "_id": {"$oid":"5ea0450ed851c30a90e70894"}, "email": "jane.doe@bosch.com", "name": "janedoe", - "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K", + "pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi", "level": "write", "location": "Rng", "device_name": "Alpha I", From 7a917c1f6bfcb3e03f1f80a6a42807e79f4842bd Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Fri, 24 Apr 2020 17:36:39 +0200 Subject: [PATCH 07/16] added PUT /user route --- oas/oas.yaml | 2 +- oas/user.yaml | 10 -- src/routes/user.spec.ts | 195 +++++++++++++++++++++++++++++++++++- src/routes/user.ts | 65 +++++++++++- src/routes/validate/user.ts | 74 +++++++++----- 5 files changed, 304 insertions(+), 42 deletions(-) diff --git a/oas/oas.yaml b/oas/oas.yaml index 03549c1..6e25698 100644 --- a/oas/oas.yaml +++ b/oas/oas.yaml @@ -23,7 +23,7 @@ info:
    • at least one digit
    • at least one lower case letter
    • at least one upper case letter
    • -
    • at least one of the following special characters: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
    • +
    • at least one of the following special characters: !"#%&'()*+,-./:;<=>?@[\]^_`{|}~
    • no whitespace
    • at least 8 characters
    diff --git a/oas/user.yaml b/oas/user.yaml index 763a051..6c2c3fc 100644 --- a/oas/user.yaml +++ b/oas/user.yaml @@ -36,14 +36,10 @@ application/json: schema: $ref: 'oas.yaml#/components/schemas/User' - 400: - $ref: 'oas.yaml#/components/responses/400' 401: $ref: 'oas.yaml#/components/responses/401' 403: $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' 500: $ref: 'oas.yaml#/components/responses/500' put: @@ -86,8 +82,6 @@ $ref: 'oas.yaml#/components/responses/401' 403: $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' 500: $ref: 'oas.yaml#/components/responses/500' delete: @@ -106,8 +100,6 @@ $ref: 'oas.yaml#/components/responses/401' 403: $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' 500: $ref: 'oas.yaml#/components/responses/500' /user/{name}: @@ -127,8 +119,6 @@ application/json: schema: $ref: 'oas.yaml#/components/schemas/User' - 400: - $ref: 'oas.yaml#/components/responses/400' 401: $ref: 'oas.yaml#/components/responses/401' 403: diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index 93b598b..46860ac 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -160,6 +160,18 @@ describe('GET /user/{name}', () => { done(); }); }); + it('returns 404 for an unknown user', done => { + supertest(server) + .get('/user/unknown') + .auth('admin', 'Abc123!#') + .expect('Content-type', /json/) + .expect(404) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Not found'}); + done(); + }); + }); }); @@ -184,7 +196,8 @@ describe('PUT /user/{name}', () => { }); it('returns own user details', done => { supertest(server) - .get('/user') + .put('/user') + .send({}) .auth('janedoe', 'Xyz890*)') .expect('Content-type', /json/) .expect(200) @@ -202,7 +215,8 @@ describe('PUT /user/{name}', () => { }); it('returns other user details for admin', done => { supertest(server) - .get('/user/janedoe') + .put('/user/janedoe') + .send({}) .auth('admin', 'Abc123!#') .expect('Content-type', /json/) .expect(200) @@ -218,9 +232,118 @@ describe('PUT /user/{name}', () => { done(); }); }); + it('changes user details as given', done => { + supertest(server) + .put('/user') + .auth('admin', 'Abc123!#') + .send({name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', device_name: 'test'}) + .expect(200) + .end(err => { + if (err) done (err); + UserModel.find({name: 'adminnew'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'adminnew'); + should(data[0]).have.property('email', 'adminnew@bosch.com'); + should(data[0]).have.property('pass').not.eql('Abc123##'); + should(data[0]).have.property('level', 'admin'); + should(data[0]).have.property('location', 'Abt'); + should(data[0]).have.property('device_name', 'test'); + done(); + }); + }); + }); + it('lets the admin change a user level', done => { + supertest(server) + .put('/user/janedoe') + .auth('admin', 'Abc123!#') + .send({level: 'read'}) + .expect(200) + .end(err => { + if (err) done (err); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('level', 'read'); + done(); + }); + }); + }); + it('does not change the level', done => { + supertest(server) + .put('/user') + .auth('janedoe', 'Xyz890*)') + .send({level: 'read'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('level', 'write'); + done(); + }); + }); + }); + it('rejects a username already in use', done => { + supertest(server) + .put('/user') + .auth('admin', 'Abc123!#') + .send({name: 'janedoe'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Username already taken'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + done(); + }); + }); + }); + it('rejects invalid user details', done => { + supertest(server) + .put('/user') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', location: 44, device_name: 'Alpha II'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); + it('rejects an invalid email address', done => { + supertest(server) + .put('/user') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); + it('rejects an invalid password', done => { + supertest(server) + .put('/user') + .auth('admin', 'Abc123!#') + .send({pass: 'password'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); it('rejects requests from non-admins for another user', done => { supertest(server) - .get('/user/admin') + .put('/user/admin') + .send({}) .auth('janedoe', 'Xyz890*)') .expect('Content-type', /json/) .expect(403) @@ -232,7 +355,8 @@ describe('PUT /user/{name}', () => { }); it('rejects requests from a user API key', done => { supertest(server) - .get('/user?key=5ea0450ed851c30a90e70899') + .put('/user?key=5ea0450ed851c30a90e70899') + .send({}) .expect('Content-type', /json/) .expect(401) .end((err, res) => { @@ -243,7 +367,8 @@ describe('PUT /user/{name}', () => { }); it('rejects requests from an admin API key', done => { supertest(server) - .get('/user/janedoe?key=5ea131671feb9c2ee0aafc9a') + .put('/user/janedoe?key=5ea131671feb9c2ee0aafc9a') + .send({}) .expect('Content-type', /json/) .expect(401) .end((err, res) => { @@ -252,6 +377,18 @@ describe('PUT /user/{name}', () => { done(); }); }); + it('returns 404 for an unknown user', done => { + supertest(server) + .put('/user/unknown') + .auth('admin', 'Abc123!#') + .send({}) + .expect(404) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Not found'}); + done(); + }); + }); }); @@ -332,6 +469,54 @@ describe('POST /user/new', () => { }); }); }); + it('rejects invalid user details', done => { + supertest(server) + .post('/user/new') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 44, device_name: 'Alpha II'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); + it('rejects an invalid user level', done => { + supertest(server) + .post('/user/new') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', device_name: 'Alpha II'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); + it('rejects an invalid email address', done => { + supertest(server) + .post('/user/new') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); + it('rejects an invalid password', done => { + supertest(server) + .post('/user/new') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); it('rejects requests from non-admins', done => { supertest(server) .post('/user/new') diff --git a/src/routes/user.ts b/src/routes/user.ts index 0865fac..d362c79 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -15,7 +15,7 @@ router.get('/users', (req, res) => { }); }); -router.get('/user/:username*?', (req, res) => { +router.get('/user/:username*?', (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; let username = req.authDetails.username; if (req.params.username !== undefined) { @@ -24,15 +24,74 @@ router.get('/user/:username*?', (req, res) => { } UserModel.findOne({name: username}).lean().exec( (err, data:any) => { - res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + if (err) next(err); + if (data) { + res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + } + else { + res.status(404).json({status: 'Not found'}); + } }); }); +router.put('/user/:username*?', (req, res, next) => { + console.log(req.authDetails); + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + let username = req.authDetails.username; + if (req.params.username !== undefined) { + if (!req.auth(res, ['admin'], 'basic')) return; + username = req.params.username; + } + const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : '')); + console.log(error); + console.log(user); + if(error !== undefined) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + if (user.hasOwnProperty('pass')) { + user.pass = bcrypt.hashSync(user.pass, 10); + } + + // check that user does not already exist if new name was specified + if (user.hasOwnProperty('name') && user.name !== username) { + UserModel.find({name: user.name}).lean().exec( (err, data:any) => { + if (err) next(err); + if (data.length > 0) { + res.status(400).json({status: 'Username already taken'}); + return; + } + + UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => { + if (err) next(err); + if (data) { + res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + } + else { + res.status(404).json({status: 'Not found'}); + } + }); + }); + } + else { + UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => { + if (err) next(err); + if (data) { + res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + } + else { + res.status(404).json({status: 'Not found'}); + } + }); + } +}); + router.post('/user/new', (req, res, next) => { if (!req.auth(res, ['admin'], 'basic')) return; // validate input - const {error, value: user} = UserValidate.input(req.body); + const {error, value: user} = UserValidate.input(req.body, 'new'); if(error !== undefined) { res.status(400).json({status: 'Invalid body format'}); return; diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index 26becdd..68f743d 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -2,34 +2,62 @@ import joi from '@hapi/joi'; import globals from '../../globals'; export default class UserValidate { // validate input for user - static input (data) { - return joi.object({ - name: joi.string() - .alphanum() - .lowercase() - .required(), + private static user = { + _id: joi.any(), + name: joi.string() + .alphanum() + .lowercase(), - email: joi.string() - .email({minDomainSegments: 2}) - .lowercase() - .required(), + email: joi.string() + .email({minDomainSegments: 2}) + .lowercase(), - pass: joi.string() - .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#$%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')) - .required(), + pass: joi.string() + .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')), - level: joi.string() - .valid(...globals.levels) - .required(), + level: joi.string() + .valid(...globals.levels), - location: joi.string() - .alphanum() - .required(), + location: joi.string() + .alphanum(), - device_name: joi.string() - .allow('') - .required() - }).validate(data); + device_name: joi.string() + .allow('') + }; + + static input (data, param) { + if (param === 'new') { + return joi.object({ + name: this.user.name.required(), + email: this.user.email.required(), + pass: this.user.pass.required(), + level: this.user.level.required(), + location: this.user.location.required(), + device_name: this.user.device_name.required() + }).validate(data); + } + else if (param === 'change') { + return joi.object({ + name: this.user.name, + email: this.user.email, + pass: this.user.pass, + location: this.user.location, + device_name: this.user.device_name + }).validate(data); + } + else if (param === 'changeadmin') { + return joi.object({ + name: this.user.name, + email: this.user.email, + pass: this.user.pass, + level: this.user.level, + location: this.user.location, + device_name: this.user.device_name + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } } static output (data) { // validate output from database for needed properties, strip everything else From eaa6484dcab12947d15fcaa36bfdaae896577e9e Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Mon, 27 Apr 2020 11:44:28 +0200 Subject: [PATCH 08/16] added test helper and rewrote tests --- .gitignore | 1 + src/helpers/test.ts | 88 ++++ src/routes/root.spec.ts | 180 ++----- src/routes/user.spec.ts | 1012 ++++++++++++++++----------------------- 4 files changed, 567 insertions(+), 714 deletions(-) create mode 100644 src/helpers/test.ts diff --git a/.gitignore b/.gitignore index 0a811ca..645d3a5 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,4 @@ dist **/.idea/tasks.xml **/.idea/shelf **/.idea/*.iml +/tmp/ diff --git a/src/helpers/test.ts b/src/helpers/test.ts new file mode 100644 index 0000000..4a537aa --- /dev/null +++ b/src/helpers/test.ts @@ -0,0 +1,88 @@ +import supertest from 'supertest'; +import should from 'should/as-function'; +import db from "../db"; + + +export default class TestHelper { + public static auth = { + admin: {pass: 'Abc123!#', key: '5ea131671feb9c2ee0aafc9a'}, + janedoe: {pass: 'Xyz890*)', key: '5ea0450ed851c30a90e70899'} + } + public static res = { + 400: {status: 'Bad request'}, + 401: {status: 'Unauthorized'}, + 403: {status: 'Forbidden'}, + 404: {status: 'Not found'}, + } + + static before (done) { + process.env.port = '2999'; + process.env.NODE_ENV = 'test'; + db.connect('test', done); + } + + static beforeEach (server, done) { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + return server + } + + static afterEach (server, done) { + server.close(done); + } + + static request (server, done, options) { // options in form: {method, url, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res} + let st = supertest(server); + if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) { + options.url += '?key=' + (this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key); + } + switch (options.method) { + case 'get': + st = st.get(options.url) + break; + case 'post': + st = st.post(options.url) + break; + case 'put': + st = st.put(options.url) + break; + case 'delete': + st = st.delete(options.url) + break; + } + if (options.hasOwnProperty('req')) { + st = st.send(options.req); + } + if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('basic')) { + if (this.auth.hasOwnProperty(options.auth.basic)) { + st = st.auth(options.auth.basic, this.auth[options.auth.basic].pass) + } + else { + st = st.auth(options.auth.basic.name, options.auth.basic.pass) + } + } + st = st.expect('Content-type', /json/) + .expect(options.httpStatus); + if (options.hasOwnProperty('res')) { + return st.end((err, res) => { + if (err) return done (err); + should(res.body).be.eql(options.res); + done(); + }); + } + else if (this.res.hasOwnProperty(options.httpStatus) && options.default !== false) { + return st.end((err, res) => { + if (err) return done (err); + should(res.body).be.eql(this.res[options.httpStatus]); + done(); + }); + } + else { + return st; + } + } +} \ No newline at end of file diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index 9372259..25be1ba 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -1,145 +1,69 @@ -import supertest from 'supertest'; -import should from 'should/as-function'; -import db from '../db'; +import TestHelper from "../helpers/test"; -describe('GET /', () => { +describe('/', () => { let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); - before(done => { - process.env.port = '2999'; - process.env.NODE_ENV = 'test'; - db.connect('test', done); - }); - beforeEach(done => { - delete require.cache[require.resolve('../index')]; // prevent loading from cache - server = require('../index'); - db.drop(err => { // reset database - if (err) return done(err); - db.loadJson(require('../test/db.json'), done); + describe('GET /', () => { + it('returns the root message', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/', + httpStatus: 200, + res: {status: 'API server up and running!'} + }); }); }); - afterEach(done => { - server.close(done); - }); - it('returns the root message', done => { - supertest(server) - .get('/') - .expect('Content-type', /json/) - .expect(200) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'API server up and running!'}); - done(); + + describe('Unknown routes', () => { + it('return a 404 message', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/unknownroute', + httpStatus: 404 }); - }); -}); - -describe('Unknown routes', () => { - let server; - - before(done => { - db.connect('test', done); - }); - beforeEach(done => { - delete require.cache[require.resolve('../index')]; // prevent loading from cache - server = require('../index'); - db.drop(err => { // reset database - if (err) return done(err); - db.loadJson(require('../test/db.json'), done); }); }); - afterEach(done => { - server.close(done); - }); - it('return a 404 message', done => { - supertest(server) - .get('/unknownroute') - .expect(404) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Not found'}); - done(); + + describe('An unauthorized request', () => { + it('returns a 401 message', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + httpStatus: 401 + }); + }); + it('does not work with correct username', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {name: 'admin', pass: 'Abc123!!'}, + httpStatus: 401 }); - }); -}); - -describe('An unauthorized request', () => { - let server; - - before(done => { - db.connect('test', done); - }); - beforeEach(done => { - delete require.cache[require.resolve('../index')]; // prevent loading from cache - server = require('../index'); - db.drop(err => { // reset database - if (err) return done(err); - db.loadJson(require('../test/db.json'), done); }); }); - afterEach(done => { - server.close(done); - }); - it('returns a 401 message', done => { - supertest(server) - .get('/authorized') - .expect(401) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Unauthorized'}); - done(); - }); - }); - it('does not work with correct username', done => { - supertest(server) - .get('/authorized') - .auth('admin', 'Abc123!!') - .expect(401) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Unauthorized'}); - done(); - }); - }); -}); -describe('An authorized request', () => { - let server; - - before(done => { - db.connect('test', done); - }); - beforeEach(done => { - delete require.cache[require.resolve('../index')]; // prevent loading from cache - server = require('../index'); - db.drop(err => { // reset database - if (err) return done(err); - db.loadJson(require('../test/db.json'), done); + describe('An authorized request', () => { + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {key: 'admin'}, + httpStatus: 200, + res: {status: 'Authorization successful', method: 'key'} + }); }); - }); - afterEach(done => { - server.close(done); - }); - it('works with an API key', done => { - supertest(server) - .get('/authorized?key=5ea131671feb9c2ee0aafc9a') - .expect(200) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Authorization successful', method: 'key'}); - done(); - }); - }); - it('works with basic auth', done => { - supertest(server) - .get('/authorized') - .auth('admin', 'Abc123!#') - .expect(200) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Authorization successful', method: 'basic'}); - done(); + it('works with basic auth', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {status: 'Authorization successful', method: 'basic'} }); + }); }); }); \ No newline at end of file diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index 46860ac..8098d9c 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -1,36 +1,23 @@ -import supertest from 'supertest'; import should from 'should/as-function'; -import db from '../db'; import UserModel from '../models/user'; +import TestHelper from "../helpers/test"; -describe('GET /users', () => { +describe('/user', () => { let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); - before(done => { - process.env.port = '2999'; - process.env.NODE_ENV = 'test'; - db.connect('test', done); - }); - beforeEach(done => { - delete require.cache[require.resolve('../index')]; // prevent loading from cache - server = require('../index'); - db.drop(err => { // reset database - if (err) return done(err); - db.loadJson(require('../test/db.json'), done); - }); - }); - afterEach(done => { - server.close(done); - }); - it('returns all users', done => { - supertest(server) - .get('/users') - .auth('admin', 'Abc123!#') - .expect('Content-type', /json/) - .expect(200) - .end((err, res) => { - if (err) done (err); + describe('GET /users', () => { + it('returns all users', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); const json = require('../test/db.json'); should(res.body).have.lengthOf(json.collections.users.length); should(res.body).matchEach(user => { @@ -44,60 +31,34 @@ describe('GET /users', () => { }); done(); }); - }); - it('rejects requests from non-admins', done => { - supertest(server) - .get('/users') - .auth('janedoe', 'Xyz890*)') - .expect('Content-type', /json/) - .expect(403) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Forbidden'}); - done(); + }); + it('rejects requests from non-admins', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + auth: {basic: 'janedoe'}, + httpStatus: 403 }); - }); - it('rejects requests from an admin API key', done => { - supertest(server) - .get('/users?key=5ea131671feb9c2ee0aafc9a') - .expect('Content-type', /json/) - .expect(401) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Unauthorized'}); - done(); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + auth: {key: 'admin'}, + httpStatus: 401 }); - }); -}); - - -describe('GET /user/{name}', () => { - let server; - - before(done => { - process.env.port = '2999'; - process.env.NODE_ENV = 'test'; - db.connect('test', done); - }); - beforeEach(done => { - delete require.cache[require.resolve('../index')]; // prevent loading from cache - server = require('../index'); - db.drop(err => { // reset database - if (err) return done(err); - db.loadJson(require('../test/db.json'), done); }); }); - afterEach(done => { - server.close(done); - }); - it('returns own user details', done => { - supertest(server) - .get('/user') - .auth('janedoe', 'Xyz890*)') - .expect('Content-type', /json/) - .expect(200) - .end((err, res) => { - if (err) done (err); + + describe('GET /user/{name}', () => { + it('returns own user details', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); should(res.body).have.property('_id').be.type('string'); should(res.body).have.property('email', 'jane.doe@bosch.com'); @@ -107,529 +68,408 @@ describe('GET /user/{name}', () => { should(res.body).have.property('device_name', 'Alpha I'); done(); }); - }); - it('returns other user details for admin', done => { - supertest(server) - .get('/user/janedoe') - .auth('admin', 'Abc123!#') - .expect('Content-type', /json/) - .expect(200) - .end((err, res) => { - if (err) done (err); - should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('email', 'jane.doe@bosch.com'); - should(res.body).have.property('name', 'janedoe'); - should(res.body).have.property('level', 'write'); - should(res.body).have.property('location', 'Rng'); - should(res.body).have.property('device_name', 'Alpha I'); - done(); + }); + it('returns other user details for admin', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('rejects requests from non-admins for another user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/admin', + auth: {basic: 'janedoe'}, + httpStatus: 403 }); - }); - it('rejects requests from non-admins for another user', done => { - supertest(server) - .get('/user/admin') - .auth('janedoe', 'Xyz890*)') - .expect('Content-type', /json/) - .expect(403) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Forbidden'}); - done(); + }); + it('rejects requests from a user API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user', + auth: {key: 'janedoe'}, + httpStatus: 401 }); - }); - it('rejects requests from a user API key', done => { - supertest(server) - .get('/user?key=5ea0450ed851c30a90e70899') - .expect('Content-type', /json/) - .expect(401) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Unauthorized'}); - done(); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/janedoe', + auth: {key: 'janedoe'}, + httpStatus: 401 }); - }); - it('rejects requests from an admin API key', done => { - supertest(server) - .get('/user/janedoe?key=5ea131671feb9c2ee0aafc9a') - .expect('Content-type', /json/) - .expect(401) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Unauthorized'}); - done(); + }); + it('returns 404 for an unknown user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/unknown', + auth: {basic: 'admin'}, + httpStatus: 404 }); - }); - it('returns 404 for an unknown user', done => { - supertest(server) - .get('/user/unknown') - .auth('admin', 'Abc123!#') - .expect('Content-type', /json/) - .expect(404) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Not found'}); - done(); - }); - }); -}); - - -describe('PUT /user/{name}', () => { - let server; - - before(done => { - process.env.port = '2999'; - process.env.NODE_ENV = 'test'; - db.connect('test', done); - }); - beforeEach(done => { - delete require.cache[require.resolve('../index')]; // prevent loading from cache - server = require('../index'); - db.drop(err => { // reset database - if (err) return done(err); - db.loadJson(require('../test/db.json'), done); }); }); - afterEach(done => { - server.close(done); - }); - it('returns own user details', done => { - supertest(server) - .put('/user') - .send({}) - .auth('janedoe', 'Xyz890*)') - .expect('Content-type', /json/) - .expect(200) - .end((err, res) => { - if (err) done (err); - should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('email', 'jane.doe@bosch.com'); - should(res.body).have.property('name', 'janedoe'); - should(res.body).have.property('level', 'write'); - should(res.body).have.property('location', 'Rng'); - should(res.body).have.property('device_name', 'Alpha I'); - done(); - }); - }); - it('returns other user details for admin', done => { - supertest(server) - .put('/user/janedoe') - .send({}) - .auth('admin', 'Abc123!#') - .expect('Content-type', /json/) - .expect(200) - .end((err, res) => { - if (err) done (err); - should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('email', 'jane.doe@bosch.com'); - should(res.body).have.property('name', 'janedoe'); - should(res.body).have.property('level', 'write'); - should(res.body).have.property('location', 'Rng'); - should(res.body).have.property('device_name', 'Alpha I'); - done(); - }); - }); - it('changes user details as given', done => { - supertest(server) - .put('/user') - .auth('admin', 'Abc123!#') - .send({name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', device_name: 'test'}) - .expect(200) - .end(err => { - if (err) done (err); - UserModel.find({name: 'adminnew'}).lean().exec( 'find', (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); - should(data[0]).have.property('_id'); - should(data[0]).have.property('name', 'adminnew'); - should(data[0]).have.property('email', 'adminnew@bosch.com'); - should(data[0]).have.property('pass').not.eql('Abc123##'); - should(data[0]).have.property('level', 'admin'); - should(data[0]).have.property('location', 'Abt'); - should(data[0]).have.property('device_name', 'test'); - done(); - }); - }); - }); - it('lets the admin change a user level', done => { - supertest(server) - .put('/user/janedoe') - .auth('admin', 'Abc123!#') - .send({level: 'read'}) - .expect(200) - .end(err => { - if (err) done (err); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.property('level', 'read'); - done(); - }); - }); - }); - it('does not change the level', done => { - supertest(server) - .put('/user') - .auth('janedoe', 'Xyz890*)') - .send({level: 'read'}) - .expect(400) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Invalid body format'}); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.property('level', 'write'); - done(); - }); - }); - }); - it('rejects a username already in use', done => { - supertest(server) - .put('/user') - .auth('admin', 'Abc123!#') - .send({name: 'janedoe'}) - .expect(400) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Username already taken'}); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - done(); - }); - }); - }); - it('rejects invalid user details', done => { - supertest(server) - .put('/user') - .auth('admin', 'Abc123!#') - .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', location: 44, device_name: 'Alpha II'}) - .expect(400) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Invalid body format'}); - done(); - }); - }); - it('rejects an invalid email address', done => { - supertest(server) - .put('/user') - .auth('admin', 'Abc123!#') - .send({email: 'john.doe'}) - .expect(400) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Invalid body format'}); - done(); - }); - }); - it('rejects an invalid password', done => { - supertest(server) - .put('/user') - .auth('admin', 'Abc123!#') - .send({pass: 'password'}) - .expect(400) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Invalid body format'}); - done(); - }); - }); - it('rejects requests from non-admins for another user', done => { - supertest(server) - .put('/user/admin') - .send({}) - .auth('janedoe', 'Xyz890*)') - .expect('Content-type', /json/) - .expect(403) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Forbidden'}); - done(); - }); - }); - it('rejects requests from a user API key', done => { - supertest(server) - .put('/user?key=5ea0450ed851c30a90e70899') - .send({}) - .expect('Content-type', /json/) - .expect(401) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Unauthorized'}); - done(); - }); - }); - it('rejects requests from an admin API key', done => { - supertest(server) - .put('/user/janedoe?key=5ea131671feb9c2ee0aafc9a') - .send({}) - .expect('Content-type', /json/) - .expect(401) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Unauthorized'}); - done(); - }); - }); - it('returns 404 for an unknown user', done => { - supertest(server) - .put('/user/unknown') - .auth('admin', 'Abc123!#') - .send({}) - .expect(404) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Not found'}); - done(); - }); - }); -}); - -describe('POST /user/new', () => { - let server; - - before(done => { - process.env.port = '2999'; - process.env.NODE_ENV = 'test'; - db.connect('test', done); - }); - beforeEach(done => { - delete require.cache[require.resolve('../index')]; // prevent loading from cache - server = require('../index'); - db.drop(err => { // reset database - if (err) return done(err); - db.loadJson(require('../test/db.json'), done); + describe('PUT /user/{name}', () => { + it('returns own user details', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); }); - }); - afterEach(done => { - server.close(done); - }); - it('returns the added user data', done => { - supertest(server) - .post('/user/new') - .auth('admin', 'Abc123!#') - .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) - .expect('Content-type', /json/) - .expect(200) - .end((err, res) => { - if (err) done (err); - should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('email', 'john.doe@bosch.com'); - should(res.body).have.property('name', 'johndoe'); - should(res.body).have.property('level', 'read'); - should(res.body).have.property('location', 'Rng'); - should(res.body).have.property('device_name', 'Alpha II'); - done(); - }); - }); - it('stores the data', done => { - supertest(server) - .post('/user/new') - .auth('admin', 'Abc123!#') - .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) - .expect(200) - .end(err => { - if (err) done (err); - UserModel.find({name: 'johndoe'}).lean().exec( 'find', (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); - should(data[0]).have.property('_id'); - should(data[0]).have.property('name', 'johndoe'); - should(data[0]).have.property('email', 'john.doe@bosch.com'); - should(data[0]).have.property('pass').not.eql('Abc123!#'); - should(data[0]).have.property('level', 'read'); - should(data[0]).have.property('location', 'Rng'); - should(data[0]).have.property('device_name', 'Alpha II'); + it('returns other user details for admin', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); done(); }); - }); - }); - it('rejects a username already in use', done => { - supertest(server) - .post('/user/new') - .auth('admin', 'Abc123!#') - .send({email: 'j.doe@bosch.com', name: 'janedoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) - .expect(400) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Username already taken'}); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - done(); - }); - }); - }); - it('rejects invalid user details', done => { - supertest(server) - .post('/user/new') - .auth('admin', 'Abc123!#') - .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 44, device_name: 'Alpha II'}) - .expect(400) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Invalid body format'}); - done(); - }); - }); - it('rejects an invalid user level', done => { - supertest(server) - .post('/user/new') - .auth('admin', 'Abc123!#') - .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', device_name: 'Alpha II'}) - .expect(400) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Invalid body format'}); - done(); - }); - }); - it('rejects an invalid email address', done => { - supertest(server) - .post('/user/new') - .auth('admin', 'Abc123!#') - .send({email: 'john.doe', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) - .expect(400) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Invalid body format'}); - done(); - }); - }); - it('rejects an invalid password', done => { - supertest(server) - .post('/user/new') - .auth('admin', 'Abc123!#') - .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'}) - .expect(400) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Invalid body format'}); - done(); - }); - }); - it('rejects requests from non-admins', done => { - supertest(server) - .post('/user/new') - .auth('janedoe', 'Xyz890*)') - .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) - .expect('Content-type', /json/) - .expect(403) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Forbidden'}); - done(); - }); - }); - it('rejects requests from an admin API key', done => { - supertest(server) - .post('/user/new?key=5ea131671feb9c2ee0aafc9a') - .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) - .expect('Content-type', /json/) - .expect(401) - .end((err, res) => { - if (err) done (err); - should(res.body).be.eql({status: 'Unauthorized'}); - done(); - }); - }); -}); - - -describe('POST /user/passreset', () => { - let server; - - before(done => { - process.env.port = '2999'; - process.env.NODE_ENV = 'test'; - db.connect('test', done); - }); - beforeEach(done => { - delete require.cache[require.resolve('../index')]; // prevent loading from cache - server = require('../index'); - db.drop(err => { // reset database - if (err) return done(err); - db.loadJson(require('../test/db.json'), done); }); - }); - afterEach(done => { - server.close(done); - }); - it('returns the ok response', done => { - supertest(server) - .post('/user/passreset') - .send({ - email: 'jane.doe@bosch.com', - name: 'janedoe' - }) - .expect('Content-type', /json/) - .expect(200) - .end((err, res) => { - if (err) done(err); - should(res.body).be.eql({status: 'OK'}); - done(); - }); - }); - it('returns 404 for wrong username/email combo', done => { - supertest(server) - .post('/user/passreset') - .send({ - email: 'jane.doe@bosch.com', - name: 'admin' - }) - .expect('Content-type', /json/) - .expect(404) - .end((err, res) => { - if (err) done(err); - should(res.body).be.eql({status: 'Not found'}); - done(); - }); - }); - it('returns 404 for unknown username', done => { - supertest(server) - .post('/user/passreset') - .send({ - email: 'jane.doe@bosch.com', - name: 'admin' - }) - .expect('Content-type', /json/) - .expect(404) - .end((err, res) => { - if (err) done(err); - should(res.body).be.eql({status: 'Not found'}); - done(); - }); - }); - it('changes the user password', done => { - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data: any) => { - if (err) return done(err); - const oldpass = data[0].pass; - supertest(server) - .post('/user/passreset') - .send({ - email: 'jane.doe@bosch.com', - name: 'janedoe' - }) - .expect('Content-type', /json/) - .expect(200) - .end((err, res) => { - if (err) done(err); - should(res.body).be.eql({status: 'OK'}); - UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { + it('changes user details as given', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', device_name: 'test'} + }).end(err => { + if (err) return done (err); + UserModel.find({name: 'adminnew'}).lean().exec( 'find', (err, data) => { if (err) return done(err); - should(data[0].pass).not.eql(oldpass); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'adminnew'); + should(data[0]).have.property('email', 'adminnew@bosch.com'); + should(data[0]).have.property('pass').not.eql('Abc123##'); + should(data[0]).have.property('level', 'admin'); + should(data[0]).have.property('location', 'Abt'); + should(data[0]).have.property('device_name', 'test'); done(); }); }); }); + it('lets the admin change a user level', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {level: 'read'} + }).end(err => { + if (err) return done (err); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('level', 'read'); + done(); + }); + }); + }); + it('does not change the level', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 400, default: false, + req: {level: 'read'} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('level', 'write'); + done(); + }); + }); + }); + it('rejects a username already in use', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {name: 'janedoe'} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'Username already taken'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + done(); + }); + }); + }); + it('rejects invalid user details', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', location: 44, device_name: 'Alpha II'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid email address', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid password', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {pass: 'password'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects requests from non-admins for another user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/admin', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects requests from a user API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('returns 404 for an unknown user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/unknown', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {} + }); + }); }); -}); + + describe('POST /user/new', () => { + it('returns the added user data', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'john.doe@bosch.com'); + should(res.body).have.property('name', 'johndoe'); + should(res.body).have.property('level', 'read'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha II'); + done(); + }); + }); + it('stores the data', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }).end(err => { + if (err) return done (err); + UserModel.find({name: 'johndoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'johndoe'); + should(data[0]).have.property('email', 'john.doe@bosch.com'); + should(data[0]).have.property('pass').not.eql('Abc123!#'); + should(data[0]).have.property('level', 'read'); + should(data[0]).have.property('location', 'Rng'); + should(data[0]).have.property('device_name', 'Alpha II'); + done(); + }); + }); + }); + it('rejects a username already in use', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {email: 'j.doe@bosch.com', name: 'janedoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'Username already taken'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + done(); + }); + }); + }); + it('rejects invalid user details', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 44, device_name: 'Alpha II'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid user level', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', device_name: 'Alpha II'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid email address', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid password', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects requests from non-admins', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {key: 'admin'}, + httpStatus: 401, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }); + }); + }); + + describe('POST /user/passreset', () => { + it('returns the ok response', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 200, + req: {email: 'jane.doe@bosch.com', name: 'janedoe'}, + res: {status: 'OK'} + }); + }); + it('returns 404 for wrong username/email combo', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 404, + req: {email: 'jane.doe@bosch.com', name: 'admin'} + }); + }); + it('returns 404 for unknown username', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 404, + req: {email: 'jane.doe@bosch.com', name: 'username'} + }); + }); + it('changes the user password', done => { + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data: any) => { + if (err) return done(err); + const oldpass = data[0].pass; + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 200, + req: {email: 'jane.doe@bosch.com', name: 'janedoe'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { + if (err) return done(err); + should(data[0].pass).not.eql(oldpass); + done(); + }); + }); + }); + }); + }); +}); \ No newline at end of file From 1eff39bb167515837406c7c757659aac620dac5c Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Mon, 27 Apr 2020 14:26:51 +0200 Subject: [PATCH 09/16] added /user/key and edited /user regex --- oas/user.yaml | 5 ++++- src/routes/user.spec.ts | 20 ++++++++++++++++++++ src/routes/user.ts | 15 +++++++++++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/oas/user.yaml b/oas/user.yaml index 6c2c3fc..c8c282d 100644 --- a/oas/user.yaml +++ b/oas/user.yaml @@ -191,7 +191,10 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/User' + properties: + key: + type: string + example: 5ea0450ed851c30a90e70899 401: $ref: 'oas.yaml#/components/responses/401' 500: diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index 8098d9c..60f5b4d 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -472,4 +472,24 @@ describe('/user', () => { }); }); }); + + describe('GET /user/key', () => { + it('returns the right API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {key: TestHelper.auth.janedoe.key} + }); + }); + it('rejects requests from an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + }) }); \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts index d362c79..26f21cc 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -15,7 +15,8 @@ router.get('/users', (req, res) => { }); }); -router.get('/user/:username*?', (req, res, next) => { +router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex + req.params.username = req.params[0]; if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; let username = req.authDetails.username; if (req.params.username !== undefined) { @@ -34,7 +35,7 @@ router.get('/user/:username*?', (req, res, next) => { }); }); -router.put('/user/:username*?', (req, res, next) => { +router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new console.log(req.authDetails); if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; let username = req.authDetails.username; @@ -87,6 +88,16 @@ router.put('/user/:username*?', (req, res, next) => { } }); +router.get('/user/key', (req, res, next) => { + console.log('hmm'); + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + + UserModel.findOne({name: req.authDetails.username}).lean().exec( (err, data:any) => { + if (err) next(err); + res.json({key: data.key}); + }); +}); + router.post('/user/new', (req, res, next) => { if (!req.auth(res, ['admin'], 'basic')) return; From 5a911a455b9d21c81d4c3267d17121115a8f6ddc Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Mon, 27 Apr 2020 15:10:14 +0200 Subject: [PATCH 10/16] added /user DELETE route --- oas/user.yaml | 16 ++-- package-lock.json | 172 ++++++++++++++++++++++++------------ package.json | 3 +- src/routes/user.spec.ts | 87 ++++++++++++++++++ src/routes/user.ts | 28 ++++-- src/routes/validate/user.ts | 6 ++ 6 files changed, 239 insertions(+), 73 deletions(-) diff --git a/oas/user.yaml b/oas/user.yaml index c8c282d..7d9d225 100644 --- a/oas/user.yaml +++ b/oas/user.yaml @@ -43,7 +43,7 @@ 500: $ref: 'oas.yaml#/components/responses/500' put: - summary: TODO change user details + summary: change user details description: 'Auth: basic, levels: read, write, maintain, admin' tags: - /user @@ -85,7 +85,7 @@ 500: $ref: 'oas.yaml#/components/responses/500' delete: - summary: TODO delete user + summary: delete user description: 'Auth: basic, levels: read, write, maintain, admin' tags: - /user @@ -94,12 +94,8 @@ responses: 200: $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' 401: $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' 500: $ref: 'oas.yaml#/components/responses/500' /user/{name}: @@ -128,7 +124,7 @@ 500: $ref: 'oas.yaml#/components/responses/500' put: - summary: TODO change user details + summary: change user details description: 'Auth: basic, levels: admin' tags: - /user @@ -158,7 +154,7 @@ 500: $ref: 'oas.yaml#/components/responses/500' delete: - summary: TODO delete user + summary: delete user description: 'Auth: basic, levels: admin' tags: - /user @@ -167,8 +163,6 @@ responses: 200: $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' 401: $ref: 'oas.yaml#/components/responses/401' 403: @@ -179,7 +173,7 @@ $ref: 'oas.yaml#/components/responses/500' /user/key: get: - summary: TODO get API key for the user + summary: get API key for the user description: 'Auth: basic, levels: read, write, maintain, dev, admin' tags: - /user diff --git a/package-lock.json b/package-lock.json index 5249707..9b82ecb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,6 +98,15 @@ "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-3.0.0.tgz", "integrity": "sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ==" }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/bson": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.2.tgz", @@ -111,6 +120,39 @@ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", + "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz", + "integrity": "sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw==", + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + }, "@types/mocha": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", @@ -139,6 +181,25 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.6.tgz", "integrity": "sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg==" }, + "@types/qs": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", + "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", + "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -745,9 +806,9 @@ } }, "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -1299,12 +1360,12 @@ "dev": true }, "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", "dev": true, "requires": { - "chalk": "^2.0.1" + "chalk": "^2.4.2" } }, "lowercase-keys": { @@ -1393,9 +1454,9 @@ } }, "mocha": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.0.0.tgz", - "integrity": "sha512-CirsOPbO3jU86YKjjMzFLcXIb5YiGLUrjrXFHoJ3e2z9vWiaZVCZQ2+gtRGMPWF+nFhN6AWwLM/juzAQ6KRkbA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz", + "integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==", "dev": true, "requires": { "ansi-colors": "3.2.3", @@ -1409,9 +1470,9 @@ "growl": "1.10.5", "he": "1.2.0", "js-yaml": "3.13.1", - "log-symbols": "2.2.0", + "log-symbols": "3.0.0", "minimatch": "3.0.4", - "mkdirp": "0.5.1", + "mkdirp": "0.5.5", "ms": "2.1.1", "node-environment-flags": "1.0.6", "object.assign": "4.1.0", @@ -1419,8 +1480,8 @@ "supports-color": "6.0.0", "which": "1.3.1", "wide-align": "1.1.3", - "yargs": "13.3.0", - "yargs-parser": "13.1.1", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", "yargs-unparser": "1.6.0" }, "dependencies": { @@ -1469,21 +1530,6 @@ "path-is-absolute": "^1.0.0" } }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -1709,9 +1755,9 @@ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" }, "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -2132,24 +2178,46 @@ "strip-ansi": "^4.0.0" } }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" } }, "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, "string_decoder": { @@ -2582,9 +2650,9 @@ "dev": true }, "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { "cliui": "^5.0.0", @@ -2596,7 +2664,7 @@ "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" + "yargs-parser": "^13.1.2" }, "dependencies": { "ansi-regex": { @@ -2628,21 +2696,13 @@ } }, "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } } }, "yargs-unparser": { diff --git a/package.json b/package.json index adc9874..3bc1031 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@apidevtools/json-schema-ref-parser": "^8.0.0", "@hapi/joi": "^17.1.1", "@types/bcrypt": "^3.0.0", + "@types/express": "^4.17.6", "@types/mocha": "^5.2.7", "@types/mongoose": "^5.7.12", "@types/node": "^13.1.6", @@ -35,7 +36,7 @@ "typescript": "^3.7.4" }, "devDependencies": { - "mocha": "^7.0.0", + "mocha": "^7.1.2", "should": "^13.2.3", "supertest": "^4.0.2" } diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index 60f5b4d..8148d2c 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -236,6 +236,16 @@ describe('/user', () => { }); }); }); + it('rejects a username which is in the special names', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}, + res: {status: 'Username already taken'} + }); + }); it('rejects invalid user details', done => { TestHelper.request(server, done, { method: 'put', @@ -304,6 +314,73 @@ describe('/user', () => { }); }); + describe('DELETE /user/{name}', () => { + it('deletes own user details', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('deletes other user details for admin', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('rejects requests from non-admins for another user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/admin', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects requests from a user API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/janedoe', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('returns 404 for an unknown user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/unknown', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + }); + describe('POST /user/new', () => { it('returns the added user data', done => { TestHelper.request(server, done, { @@ -365,6 +442,16 @@ describe('/user', () => { }); }); }); + it('rejects a username which is in the special names', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}, + res: {status: 'Username already taken'} + }); + }); it('rejects invalid user details', done => { TestHelper.request(server, done, { method: 'post', diff --git a/src/routes/user.ts b/src/routes/user.ts index 26f21cc..f4326ea 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -36,7 +36,7 @@ router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi }); router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new - console.log(req.authDetails); + req.params.username = req.params[0]; if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; let username = req.authDetails.username; if (req.params.username !== undefined) { @@ -44,8 +44,6 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi username = req.params.username; } const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : '')); - console.log(error); - console.log(user); if(error !== undefined) { res.status(400).json({status: 'Invalid body format'}); return; @@ -59,7 +57,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi if (user.hasOwnProperty('name') && user.name !== username) { UserModel.find({name: user.name}).lean().exec( (err, data:any) => { if (err) next(err); - if (data.length > 0) { + if (data.length > 0 || UserValidate.isSpecialName(user.name)) { res.status(400).json({status: 'Username already taken'}); return; } @@ -88,6 +86,26 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi } }); +router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex + req.params.username = req.params[0]; + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + let username = req.authDetails.username; + if (req.params.username !== undefined) { + if (!req.auth(res, ['admin'], 'basic')) return; + username = req.params.username; + } + + UserModel.findOneAndDelete({name: username}).lean().exec( (err, data:any) => { + if (err) next(err); + if (data) { + res.json({status: 'OK'}) + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + router.get('/user/key', (req, res, next) => { console.log('hmm'); if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; @@ -111,7 +129,7 @@ router.post('/user/new', (req, res, next) => { // check that user does not already exist UserModel.find({name: user.name}).lean().exec( (err, data:any) => { if (err) next(err); - if (data.length > 0) { + if (data.length > 0 || UserValidate.isSpecialName(user.name)) { res.status(400).json({status: 'Username already taken'}); return; } diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index 68f743d..4f563c6 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -25,6 +25,8 @@ export default class UserValidate { // validate input for user .allow('') }; + private static specialUsernames = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take + static input (data, param) { if (param === 'new') { return joi.object({ @@ -71,4 +73,8 @@ export default class UserValidate { // validate input for user }).validate(data, {stripUnknown: true}) return error !== undefined? null : value; } + + static isSpecialName (name) { // true if name belongs to special names + return this.specialUsernames.indexOf(name) > -1; + } } From 8b355d1d6570c2d46ac667a57ac6f873c6e39f8b Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Mon, 27 Apr 2020 15:24:17 +0200 Subject: [PATCH 11/16] added custom type definitions --- package-lock.json | 11 ---- package.json | 5 +- src/customTypes/express.ts | 116 +++++++++++++++++++++++++++++++++++++ tsconfig.json | 6 +- 4 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 src/customTypes/express.ts diff --git a/package-lock.json b/package-lock.json index 9b82ecb..839b669 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,17 +128,6 @@ "@types/node": "*" } }, - "@types/express": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", - "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, "@types/express-serve-static-core": { "version": "4.17.5", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz", diff --git a/package.json b/package.json index 3bc1031..4753647 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,13 @@ "@apidevtools/json-schema-ref-parser": "^8.0.0", "@hapi/joi": "^17.1.1", "@types/bcrypt": "^3.0.0", - "@types/express": "^4.17.6", + "@types/body-parser": "^1.19.0", + "@types/express-serve-static-core": "^4.17.5", "@types/mocha": "^5.2.7", "@types/mongoose": "^5.7.12", "@types/node": "^13.1.6", + "@types/qs": "^6.9.1", + "@types/serve-static": "^1.13.3", "axios": "^0.19.2", "basic-auth": "^2.0.1", "bcryptjs": "^2.4.3", diff --git a/src/customTypes/express.ts b/src/customTypes/express.ts new file mode 100644 index 0000000..361c961 --- /dev/null +++ b/src/customTypes/express.ts @@ -0,0 +1,116 @@ +// Type definitions for Express 4.17 +// Project: http://expressjs.com +// Definitions by: Boris Yankov +// China Medical University Hospital +// Puneet Arora +// Dylan Frankland +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.3 + +/* =================== USAGE =================== + + import * as express from "express"; + var app = express(); + + =============================================== */ + +/// +/// + +import * as bodyParser from "body-parser"; +import serveStatic = require("serve-static"); +import * as core from "express-serve-static-core"; +import * as qs from "qs"; + +/** + * Creates an Express application. The express() function is a top-level function exported by the express module. + */ +declare function e(): core.Express; + +declare namespace e { + /** + * This is a built-in middleware function in Express. It parses incoming requests with JSON payloads and is based on body-parser. + * @since 4.16.0 + */ + var json: typeof bodyParser.json; + + /** + * This is a built-in middleware function in Express. It parses incoming requests with Buffer payloads and is based on body-parser. + * @since 4.17.0 + */ + var raw: typeof bodyParser.raw; + + /** + * This is a built-in middleware function in Express. It parses incoming requests with text payloads and is based on body-parser. + * @since 4.17.0 + */ + var text: typeof bodyParser.text; + + /** + * These are the exposed prototypes. + */ + var application: Application; + var request: Request; + var response: Response; + + /** + * This is a built-in middleware function in Express. It serves static files and is based on serve-static. + */ + var static: typeof serveStatic; + + /** + * This is a built-in middleware function in Express. It parses incoming requests with urlencoded payloads and is based on body-parser. + * @since 4.16.0 + */ + var urlencoded: typeof bodyParser.urlencoded; + + /** + * This is a built-in middleware function in Express. It parses incoming request query parameters. + */ + export function query(options: qs.IParseOptions | typeof qs.parse): Handler; + + export function Router(options?: RouterOptions): core.Router; + + interface RouterOptions { + /** + * Enable case sensitivity. + */ + caseSensitive?: boolean; + + /** + * Preserve the req.params values from the parent router. + * If the parent and the child have conflicting param names, the child’s value take precedence. + * + * @default false + * @since 4.5.0 + */ + mergeParams?: boolean; + + /** + * Enable strict routing. + */ + strict?: boolean; + } + + interface Application extends core.Application { } + interface CookieOptions extends core.CookieOptions { } + interface Errback extends core.Errback { } + interface ErrorRequestHandler

    + extends core.ErrorRequestHandler { } + interface Express extends core.Express { } + interface Handler extends core.Handler { } + interface IRoute extends core.IRoute { } + interface IRouter extends core.IRouter { } + interface IRouterHandler extends core.IRouterHandler { } + interface IRouterMatcher extends core.IRouterMatcher { } + interface MediaType extends core.MediaType { } + interface NextFunction extends core.NextFunction { } + interface Request

    extends core.Request { } + interface RequestHandler

    extends core.RequestHandler { } + interface RequestParamHandler extends core.RequestParamHandler { } + export interface Response extends core.Response { } + interface Router extends core.Router { } + interface Send extends core.Send { } +} + +export = e; diff --git a/tsconfig.json b/tsconfig.json index 304952d..8bbe445 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,11 @@ "outDir": "dist", "sourceMap": true, "esModuleInterop": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "typeRoots": [ + "src/customTypings", + "node_modules/@types" + ] }, "files": [ "./node_modules/@types/node/index.d.ts" From 600385cede329d3d6f0d09b76b4ad7b6a2e87048 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Wed, 29 Apr 2020 12:10:27 +0200 Subject: [PATCH 12/16] added /materials route --- .idea/inspectionProfiles/Project_Default.xml | 1 + oas/oas.yaml => api/api.yaml | 0 {oas => api}/condition.yaml | 38 +- api/material.yaml | 121 ++++++ {oas => api}/measurement.yaml | 38 +- {oas => api}/model.yaml | 34 +- {oas => api}/others.yaml | 6 +- {oas => api}/parameters.yaml | 1 + {oas => api}/responses.yaml | 0 {oas => api}/sample.yaml | 48 +-- {oas => api}/schemas.yaml | 94 ++--- {oas => api}/template.yaml | 88 ++--- {oas => api}/user.yaml | 100 ++--- oas/material.yaml | 67 ---- src/helpers/test.ts | 5 +- src/index.ts | 13 +- src/models/material.ts | 16 + src/routes/material.spec.ts | 387 +++++++++++++++++++ src/routes/material.ts | 59 +++ src/routes/user.spec.ts | 102 +++-- src/routes/user.ts | 3 + src/routes/validate/id.ts | 17 + src/routes/validate/material.ts | 82 ++++ src/routes/validate/user.ts | 31 +- src/test/db.json | 69 +++- static/styles/swagger.css | 0 26 files changed, 1081 insertions(+), 339 deletions(-) rename oas/oas.yaml => api/api.yaml (100%) rename {oas => api}/condition.yaml (51%) create mode 100644 api/material.yaml rename {oas => api}/measurement.yaml (52%) rename {oas => api}/model.yaml (54%) rename {oas => api}/others.yaml (85%) rename {oas => api}/parameters.yaml (79%) rename {oas => api}/responses.yaml (100%) rename {oas => api}/sample.yaml (59%) rename {oas => api}/schemas.yaml (55%) rename {oas => api}/template.yaml (66%) rename {oas => api}/user.yaml (60%) delete mode 100644 oas/material.yaml create mode 100644 src/models/material.ts create mode 100644 src/routes/material.spec.ts create mode 100644 src/routes/material.ts create mode 100644 src/routes/validate/id.ts create mode 100644 src/routes/validate/material.ts create mode 100644 static/styles/swagger.css diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index c947305..7e46df7 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,5 +2,6 @@ \ No newline at end of file diff --git a/oas/oas.yaml b/api/api.yaml similarity index 100% rename from oas/oas.yaml rename to api/api.yaml diff --git a/oas/condition.yaml b/api/condition.yaml similarity index 51% rename from oas/condition.yaml rename to api/condition.yaml index cca8ca6..5efa2ac 100644 --- a/oas/condition.yaml +++ b/api/condition.yaml @@ -1,6 +1,6 @@ /condition/{id}: parameters: - - $ref: 'oas.yaml#/components/parameters/Id' + - $ref: 'api.yaml#/components/parameters/Id' get: summary: TODO condition by id description: 'Auth: all, levels: read, write, maintain, dev, admin' @@ -12,15 +12,15 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/Condition' + $ref: 'api.yaml#/components/schemas/Condition' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' put: summary: TODO add/change condition description: 'Auth: basic, levels: write, maintain, dev, admin' @@ -33,24 +33,24 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/Condition' + $ref: 'api.yaml#/components/schemas/Condition' responses: 200: description: condition details content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/Condition' + $ref: 'api.yaml#/components/schemas/Condition' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' delete: summary: TODO delete condition description: 'Auth: basic, levels: write, maintain, dev, admin' @@ -60,14 +60,14 @@ - BasicAuth: [] responses: 200: - $ref: 'oas.yaml#/components/responses/Ok' + $ref: 'api.yaml#/components/responses/Ok' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/api/material.yaml b/api/material.yaml new file mode 100644 index 0000000..c32c1ab --- /dev/null +++ b/api/material.yaml @@ -0,0 +1,121 @@ +/materials: + get: + summary: lists all materials + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - /material + responses: + 200: + description: all material details + content: + application/json: + schema: + type: array + items: + $ref: 'api.yaml#/components/schemas/Material' + 401: + $ref: 'api.yaml#/components/responses/401' + 500: + $ref: 'api.yaml#/components/responses/500' + +/material/{id}: + parameters: + - $ref: 'api.yaml#/components/parameters/Id' + get: + summary: get material details + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - /material + responses: + 200: + description: material details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Material' + 401: + $ref: 'api.yaml#/components/responses/401' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + put: + summary: TODO change material + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /material + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Material' + responses: + 200: + description: material details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Material' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + delete: + summary: TODO delete material + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /material + security: + - BasicAuth: [] + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + +/material/new: + post: + summary: TODO add material + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /material + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Material' + responses: + 200: + description: material details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Material' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 500: + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/measurement.yaml b/api/measurement.yaml similarity index 52% rename from oas/measurement.yaml rename to api/measurement.yaml index 0b4d5b2..0f86047 100644 --- a/oas/measurement.yaml +++ b/api/measurement.yaml @@ -1,6 +1,6 @@ /measurement/{id}: parameters: - - $ref: 'oas.yaml#/components/parameters/Id' + - $ref: 'api.yaml#/components/parameters/Id' get: summary: TODO measurement values by id description: 'Auth: all, levels: read, write, maintain, dev, admin' @@ -12,15 +12,15 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/Measurement' + $ref: 'api.yaml#/components/schemas/Measurement' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' put: summary: TODO add/change measurement description: 'Auth: basic, levels: write, maintain, dev, admin' @@ -33,24 +33,24 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/Measurement' + $ref: 'api.yaml#/components/schemas/Measurement' responses: 200: description: measurement details content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/Measurement' + $ref: 'api.yaml#/components/schemas/Measurement' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' delete: summary: TODO delete measurement description: 'Auth: basic, levels: write, maintain, dev, admin' @@ -60,14 +60,14 @@ - BasicAuth: [] responses: 200: - $ref: 'oas.yaml#/components/responses/Ok' + $ref: 'api.yaml#/components/responses/Ok' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/model.yaml b/api/model.yaml similarity index 54% rename from oas/model.yaml rename to api/model.yaml index 24df9af..f9c3d72 100644 --- a/oas/model.yaml +++ b/api/model.yaml @@ -1,6 +1,6 @@ /model/{name}: parameters: - - $ref: 'oas.yaml#/components/parameters/Name' + - $ref: 'api.yaml#/components/parameters/Name' get: summary: TODO get model data by name description: 'Auth: all, levels: dev, admin' @@ -15,13 +15,13 @@ type: string format: binary 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' put: summary: TODO add/replace model data by name description: 'Auth: all, levels: dev, admin' @@ -37,17 +37,17 @@ format: binary responses: 200: - $ref: 'oas.yaml#/components/responses/Ok' + $ref: 'api.yaml#/components/responses/Ok' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' delete: summary: TODO delete model data description: 'Auth: basic, levels: dev, admin' @@ -57,14 +57,14 @@ - BasicAuth: [] responses: 200: - $ref: 'oas.yaml#/components/responses/Ok' + $ref: 'api.yaml#/components/responses/Ok' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/others.yaml b/api/others.yaml similarity index 85% rename from oas/others.yaml rename to api/others.yaml index c543797..a953bf8 100644 --- a/oas/others.yaml +++ b/api/others.yaml @@ -16,7 +16,7 @@ type: string example: 'API server up and running!' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /authorized: get: @@ -38,6 +38,6 @@ type: string example: 'basic' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/parameters.yaml b/api/parameters.yaml similarity index 79% rename from oas/parameters.yaml rename to api/parameters.yaml index 659808f..f370c13 100644 --- a/oas/parameters.yaml +++ b/api/parameters.yaml @@ -4,6 +4,7 @@ Id: required: true schema: type: string + example: 5ea0450ed851c30a90e70894 Name: name: name in: path diff --git a/oas/responses.yaml b/api/responses.yaml similarity index 100% rename from oas/responses.yaml rename to api/responses.yaml diff --git a/oas/sample.yaml b/api/sample.yaml similarity index 59% rename from oas/sample.yaml rename to api/sample.yaml index b84be19..e127d74 100644 --- a/oas/sample.yaml +++ b/api/sample.yaml @@ -10,14 +10,14 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/Samples' + $ref: 'api.yaml#/components/schemas/Samples' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /sample/{id}: parameters: - - $ref: 'oas.yaml#/components/parameters/Id' + - $ref: 'api.yaml#/components/parameters/Id' get: summary: TODO sample details description: 'Auth: all, levels: read, write, maintain, dev, admin' @@ -29,15 +29,15 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/SampleDetail' + $ref: 'api.yaml#/components/schemas/SampleDetail' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' put: summary: TODO add/change sample description: 'Auth: basic, levels: write, maintain, dev, admin' @@ -50,24 +50,24 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/Sample' + $ref: 'api.yaml#/components/schemas/Sample' responses: 200: description: samples details content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/SampleDetail' + $ref: 'api.yaml#/components/schemas/SampleDetail' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' delete: summary: TODO delete sample description: 'Auth: basic, levels: write, maintain, dev, admin' @@ -77,17 +77,17 @@ - BasicAuth: [] responses: 200: - $ref: 'oas.yaml#/components/responses/Ok' + $ref: 'api.yaml#/components/responses/Ok' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /sample/notes/fields: get: summary: TODO list all existing field names for custom notes fields @@ -107,6 +107,6 @@ type: number example: 20 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/schemas.yaml b/api/schemas.yaml similarity index 55% rename from oas/schemas.yaml rename to api/schemas.yaml index 4d8a805..62b4690 100644 --- a/oas/schemas.yaml +++ b/api/schemas.yaml @@ -5,12 +5,13 @@ _Id: properties: _id: allOf: - - $ref: 'oas.yaml#/components/schemas/Id' + - $ref: 'api.yaml#/components/schemas/Id' readOnly: true Color: properties: color: type: string + example: black SampleProperties: properties: sample_number: @@ -24,24 +25,24 @@ SampleProperties: Samples: allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - - $ref: 'oas.yaml#/components/schemas/Color' - - $ref: 'oas.yaml#/components/schemas/SampleProperties' + - $ref: 'api.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/Color' + - $ref: 'api.yaml#/components/schemas/SampleProperties' properties: material_id: - $ref: 'oas.yaml#/components/schemas/Id' + $ref: 'api.yaml#/components/schemas/Id' note_id: - $ref: 'oas.yaml#/components/schemas/Id' + $ref: 'api.yaml#/components/schemas/Id' user_id: - $ref: 'oas.yaml#/components/schemas/Id' + $ref: 'api.yaml#/components/schemas/Id' Sample: allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - - $ref: 'oas.yaml#/components/schemas/Color' - - $ref: 'oas.yaml#/components/schemas/SampleProperties' + - $ref: 'api.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/Color' + - $ref: 'api.yaml#/components/schemas/SampleProperties' properties: material: - $ref: 'oas.yaml#/components/schemas/Material' + $ref: 'api.yaml#/components/schemas/Material' notes: type: object properties: @@ -50,15 +51,15 @@ Sample: sample_references: type: array items: - $ref: 'oas.yaml#/components/schemas/Id' + $ref: 'api.yaml#/components/schemas/Id' SampleDetail: allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - - $ref: 'oas.yaml#/components/schemas/Color' - - $ref: 'oas.yaml#/components/schemas/SampleProperties' + - $ref: 'api.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/Color' + - $ref: 'api.yaml#/components/schemas/SampleProperties' properties: material: - $ref: 'oas.yaml#/components/schemas/Material' + $ref: 'api.yaml#/components/schemas/Material' notes: type: object properties: @@ -67,63 +68,70 @@ SampleDetail: sample_references: type: array items: - $ref: 'oas.yaml#/components/schemas/Id' + $ref: 'api.yaml#/components/schemas/Id' conditions: type: array items: - $ref: 'oas.yaml#/components/schemas/Condition' + $ref: 'api.yaml#/components/schemas/Condition' Material: allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/_Id' properties: - material_numbers: + name: + type: string + example: Stanyl TW 200 F8 + supplier: + type: string + example: DSM + group: + type: string + example: PA46 + mineral: + type: number + example: 0 + glass_fiber: + type: number + example: 40 + carbon_fiber: + type: number + example: 0 + numbers: type: array items: type: object allOf: - - $ref: 'oas.yaml#/components/schemas/Color' + - $ref: 'api.yaml#/components/schemas/Color' properties: number: type: number - material_group: - type: string - supplier: - type: string - material_name: - type: string - mineral: - type: number - glass_fiber: - type: number - carbon_fiber: - type: number + example: 5514263423 Condition: allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/_Id' properties: sample_id: - $ref: 'oas.yaml#/components/schemas/Id' + $ref: 'api.yaml#/components/schemas/Id' parameters: type: object treatment_template: - $ref: 'oas.yaml#/components/schemas/Id' + $ref: 'api.yaml#/components/schemas/Id' Measurement: allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/_Id' properties: condition_id: - $ref: 'oas.yaml#/components/schemas/Id' + $ref: 'api.yaml#/components/schemas/Id' values: type: object measurement_template: - $ref: 'oas.yaml#/components/schemas/Id' + $ref: 'api.yaml#/components/schemas/Id' Template: allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/_Id' properties: name: type: string @@ -149,9 +157,9 @@ UserName: example: johndoe User: allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - - $ref: 'oas.yaml#/components/schemas/UserName' - - $ref: 'oas.yaml#/components/schemas/Email' + - $ref: 'api.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/UserName' + - $ref: 'api.yaml#/components/schemas/Email' properties: pass: type: string diff --git a/oas/template.yaml b/api/template.yaml similarity index 66% rename from oas/template.yaml rename to api/template.yaml index a09cb21..9219d81 100644 --- a/oas/template.yaml +++ b/api/template.yaml @@ -14,7 +14,7 @@ schema: type: array items: - $ref: 'oas.yaml#/components/schemas/Template' + $ref: 'api.yaml#/components/schemas/Template' example: name: heat aging parameters: @@ -22,12 +22,12 @@ range: - copper 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /templates/treatment/{name}: parameters: - - $ref: 'oas.yaml#/components/parameters/Name' + - $ref: 'api.yaml#/components/parameters/Name' get: summary: TODO treatment method details description: 'Auth: basic, levels: read, write, maintain, admin' @@ -42,7 +42,7 @@ application/json: schema: allOf: - - $ref: 'oas.yaml#/components/schemas/Template' + - $ref: 'api.yaml#/components/schemas/Template' example: name: heat aging parameters: @@ -50,13 +50,13 @@ range: - copper 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' put: summary: TODO add/change treatment method description: 'Auth: basic, levels: maintain, admin' @@ -70,7 +70,7 @@ application/json: schema: allOf: - - $ref: 'oas.yaml#/components/schemas/Template' + - $ref: 'api.yaml#/components/schemas/Template' example: name: heat aging parameters: @@ -84,7 +84,7 @@ application/json: schema: allOf: - - $ref: 'oas.yaml#/components/schemas/Template' + - $ref: 'api.yaml#/components/schemas/Template' example: name: heat aging parameters: @@ -92,15 +92,15 @@ range: - copper 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' delete: summary: TODO delete treatment method description: 'Auth: basic, levels: maintain, admin' @@ -110,17 +110,17 @@ - BasicAuth: [] responses: 200: - $ref: 'oas.yaml#/components/responses/Ok' + $ref: 'api.yaml#/components/responses/Ok' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /template/measurements: get: summary: TODO all available measurement methods @@ -137,7 +137,7 @@ schema: type: array items: - $ref: 'oas.yaml#/components/schemas/Template' + $ref: 'api.yaml#/components/schemas/Template' example: name: humidity parameters: @@ -146,12 +146,12 @@ min: 0 max: 2 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /templates/measurement/{name}: parameters: - - $ref: 'oas.yaml#/components/parameters/Name' + - $ref: 'api.yaml#/components/parameters/Name' get: summary: TODO measurement method details description: 'Auth: basic, levels: read, write, maintain, admin' @@ -166,7 +166,7 @@ application/json: schema: allOf: - - $ref: 'oas.yaml#/components/schemas/Template' + - $ref: 'api.yaml#/components/schemas/Template' example: name: humidity parameters: @@ -175,13 +175,13 @@ min: 0 max: 2 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' put: summary: TODO add/change measurement method description: 'Auth: basic, levels: maintain, admin' @@ -195,7 +195,7 @@ application/json: schema: allOf: - - $ref: 'oas.yaml#/components/schemas/Template' + - $ref: 'api.yaml#/components/schemas/Template' example: name: humidity parameters: @@ -210,7 +210,7 @@ application/json: schema: allOf: - - $ref: 'oas.yaml#/components/schemas/Template' + - $ref: 'api.yaml#/components/schemas/Template' example: name: humidity parameters: @@ -219,15 +219,15 @@ min: 0 max: 2 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' delete: summary: TODO delete measurement method description: 'Auth: basic, levels: maintain, admin' @@ -237,14 +237,14 @@ - BasicAuth: [] responses: 200: - $ref: 'oas.yaml#/components/responses/Ok' + $ref: 'api.yaml#/components/responses/Ok' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/user.yaml b/api/user.yaml similarity index 60% rename from oas/user.yaml rename to api/user.yaml index 7d9d225..757ebf0 100644 --- a/oas/user.yaml +++ b/api/user.yaml @@ -14,13 +14,13 @@ schema: type: array items: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'api.yaml#/components/schemas/User' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /user: get: summary: list own user details @@ -35,13 +35,13 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'api.yaml#/components/schemas/User' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' put: summary: change user details description: 'Auth: basic, levels: read, write, maintain, admin' @@ -55,9 +55,9 @@ application/json: schema: allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - - $ref: 'oas.yaml#/components/schemas/UserName' - - $ref: 'oas.yaml#/components/schemas/Email' + - $ref: 'api.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/UserName' + - $ref: 'api.yaml#/components/schemas/Email' properties: pass: type: string @@ -75,15 +75,15 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'api.yaml#/components/schemas/User' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' delete: summary: delete user description: 'Auth: basic, levels: read, write, maintain, admin' @@ -93,14 +93,14 @@ - BasicAuth: [] responses: 200: - $ref: 'oas.yaml#/components/responses/Ok' + $ref: 'api.yaml#/components/responses/Ok' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /user/{name}: parameters: - - $ref: 'oas.yaml#/components/parameters/Name' + - $ref: 'api.yaml#/components/parameters/Name' get: summary: list user details description: 'Auth: basic, levels: admin' @@ -114,15 +114,15 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'api.yaml#/components/schemas/User' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' put: summary: change user details description: 'Auth: basic, levels: admin' @@ -135,24 +135,24 @@ content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'api.yaml#/components/schemas/User' responses: 200: description: user details content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'api.yaml#/components/schemas/User' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' delete: summary: delete user description: 'Auth: basic, levels: admin' @@ -162,15 +162,15 @@ - BasicAuth: [] responses: 200: - $ref: 'oas.yaml#/components/responses/Ok' + $ref: 'api.yaml#/components/responses/Ok' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /user/key: get: summary: get API key for the user @@ -190,9 +190,9 @@ type: string example: 5ea0450ed851c30a90e70899 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /user/new: post: summary: add new user @@ -214,22 +214,22 @@ - location - device_name allOf: - - $ref: 'oas.yaml#/components/schemas/User' + - $ref: 'api.yaml#/components/schemas/User' responses: 200: description: user details content: application/json: schema: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'api.yaml#/components/schemas/User' 400: - $ref: 'oas.yaml#/components/responses/400' + $ref: 'api.yaml#/components/responses/400' 401: - $ref: 'oas.yaml#/components/responses/401' + $ref: 'api.yaml#/components/responses/401' 403: - $ref: 'oas.yaml#/components/responses/403' + $ref: 'api.yaml#/components/responses/403' 500: - $ref: 'oas.yaml#/components/responses/500' + $ref: 'api.yaml#/components/responses/500' /user/passreset: post: summary: reset password and send mail to restore @@ -244,12 +244,12 @@ application/json: schema: allOf: - - $ref: 'oas.yaml#/components/schemas/UserName' - - $ref: 'oas.yaml#/components/schemas/Email' + - $ref: 'api.yaml#/components/schemas/UserName' + - $ref: 'api.yaml#/components/schemas/Email' responses: 200: - $ref: 'oas.yaml#/components/responses/Ok' + $ref: 'api.yaml#/components/responses/Ok' 404: - $ref: 'oas.yaml#/components/responses/404' + $ref: 'api.yaml#/components/responses/404' 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/material.yaml b/oas/material.yaml deleted file mode 100644 index d5d7d34..0000000 --- a/oas/material.yaml +++ /dev/null @@ -1,67 +0,0 @@ -/material/{id}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Id' - get: - summary: TODO get material details - description: 'Auth: all, levels: read, write, maintain, dev, admin' - tags: - - /material - responses: - 200: - description: created material - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Material' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 500: - $ref: 'oas.yaml#/components/responses/500' - put: - summary: TODO add/change material - description: 'Auth: basic, levels: write, maintain, dev, admin' - tags: - - /material - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Material' - responses: - 200: - description: material details - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Material' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 500: - $ref: 'oas.yaml#/components/responses/500' - delete: - summary: TODO delete material - description: 'Auth: basic, levels: write, maintain, dev, admin' - tags: - - /material - security: - - BasicAuth: [] - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/src/helpers/test.ts b/src/helpers/test.ts index 4a537aa..afd49dd 100644 --- a/src/helpers/test.ts +++ b/src/helpers/test.ts @@ -5,8 +5,9 @@ import db from "../db"; export default class TestHelper { public static auth = { - admin: {pass: 'Abc123!#', key: '5ea131671feb9c2ee0aafc9a'}, - janedoe: {pass: 'Xyz890*)', key: '5ea0450ed851c30a90e70899'} + admin: {pass: 'Abc123!#', key: '000000000000000000001003'}, + janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002'}, + user: {pass: 'Xyz890*)', key: '000000000000000000001001'} } public static res = { 400: {status: 'Bad request'}, diff --git a/src/index.ts b/src/index.ts index 67e29e1..e8cf691 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,16 +46,17 @@ app.use(require('./helpers/authorize')); // handle authentication // require routes app.use('/', require('./routes/root')); app.use('/', require('./routes/user')); +app.use('/', require('./routes/material')); // Swagger UI -let oasDoc: JSONSchema = {}; -jsonRefParser.bundle('oas/oas.yaml', (err, doc) => { +let apiDoc: JSONSchema = {}; +jsonRefParser.bundle('api/api.yaml', (err, doc) => { if(err) throw err; - oasDoc = doc; - oasDoc.paths = oasDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); - swagger.setup(oasDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'}); + apiDoc = doc; + apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); + swagger.setup(apiDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'}); }); -app.use('/api', swagger.serve, swagger.setup(oasDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'})); +app.use('/api', swagger.serve, swagger.setup(apiDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'})); app.use((req, res) => { // 404 error handling res.status(404).json({status: 'Not found'}); diff --git a/src/models/material.ts b/src/models/material.ts new file mode 100644 index 0000000..530f8f0 --- /dev/null +++ b/src/models/material.ts @@ -0,0 +1,16 @@ +import mongoose from 'mongoose'; + +const MaterialSchema = new mongoose.Schema({ + name: {type: String, index: {unique: true}}, + supplier: String, + group: String, + mineral: String, + glass_fiber: String, + carbon_fiber: String, + numbers: [{ + color: String, + number: Number + }] +}); + +export default mongoose.model('material', MaterialSchema); \ No newline at end of file diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts new file mode 100644 index 0000000..bb82bc1 --- /dev/null +++ b/src/routes/material.spec.ts @@ -0,0 +1,387 @@ +import should from 'should/as-function'; +import MaterialModel from '../models/material'; +import TestHelper from "../helpers/test"; + + +describe('/material', () => { + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + + describe('GET /materials', () => { + it('returns all materials', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.users.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(material).have.property('_id').be.type('string'); + should(material).have.property('name').be.type('string'); + should(material).have.property('supplier').be.type('string'); + should(material).have.property('group').be.type('string'); + should(material).have.property('mineral').be.type('number'); + should(material).have.property('glass_fiber').be.type('number'); + should(material).have.property('carbon_fiber').be.type('number'); + should(material.numbers).matchEach(number => { + should(number).have.only.keys('color', 'number'); + should(number).have.property('color').be.type('string'); + should(number).have.property('number').be.type('number'); + }); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials', + auth: {key: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.users.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(material).have.property('_id').be.type('string'); + should(material).have.property('name').be.type('string'); + should(material).have.property('supplier').be.type('string'); + should(material).have.property('group').be.type('string'); + should(material).have.property('mineral').be.type('number'); + should(material).have.property('glass_fiber').be.type('number'); + should(material).have.property('carbon_fiber').be.type('number'); + should(material.numbers).matchEach(number => { + should(number).have.only.keys('color', 'number'); + should(number).have.property('color').be.type('string'); + should(number).have.property('number').be.type('number'); + }); + }); + done(); + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials', + httpStatus: 401 + }); + }); + }); + + describe('GET /material/{id}', () => { + it('returns the right material', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]} + }); + }); + it('returns the right material for an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000003', + auth: {key: 'admin'}, + httpStatus: 200, + res: {_id: '100000000000000000000003', name: 'PA GF 50 black (2706)', supplier: 'Akro-Plastic', group: 'PA66+PA6I/6T', mineral: 0, glass_fiber: 0, carbon_fiber: 0, numbers: []} + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/10000000000000000000000x', + auth: {key: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects an unknown id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000111', + auth: {key: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000001', + httpStatus: 401 + }); + }); + }); + + describe('PUT /material/{id}', () => { + it('returns the right material', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {}, + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]} + }); + }); + it('changes the given properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]} + , + }).end(err => { + if (err) return done(err); + MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { + if (err) return done(err); + should(data).be.eql({_id: '100000000000000000000002', name: 'UltramidTKR4355G7', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}], __v: 0} + ); + done(); + }); + }); + }); + it('rejects already existing material names', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'UltramidTKR4355G7'}, + res: {status: 'Material name already taken'} + }); + }); + it('rejects wrong material properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {mineral: 'x', glass_fiber: 'x', carbon_fiber: 'x', numbers: [{colorxx: 'black', number: 'xxx'}]}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/10000000000000000000000x', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {}, + res: {status: 'Invalid id'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000002', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000002', + auth: {basic: 'user'}, + httpStatus: 403, + req: {} + }); + }); + it('returns 404 for an unknown material', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000111', + auth: {basic: 'janedoe'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + httpStatus: 401, + req: {} + }); + }); + }); + + describe('DELETE /material/{id}', () => { + it('deletes the material', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('rejects deleting a material referenced by samples'); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/10000000000000000000000x', + auth: {basic: 'admin'}, + httpStatus: 400, + res: {status: 'Invalid id'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000002', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000002', + auth: {basic: 'user'}, + httpStatus: 403 + }); + }); + it('returns 404 for an unknown id', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000111', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000001', + httpStatus: 401 + }); + }); + }); + + describe('POST /material/new', () => { + it('returns the right material', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]} + }).end((err, res) => { + if (err) return done (err); + console.log(res.body); + should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('name', 'Crastin CE 2510'); + should(res.body).have.property('supplier', 'Du Pont'); + should(res.body).have.property('group', 'PBT'); + should(res.body).have.property('mineral', 0); + should(res.body).have.property('glass_fiber', 30); + should(res.body).have.property('carbon_fiber', 0); + should(res.body.numbers).matchEach(number => { + should(number).have.only.keys('color', 'number'); + should(number).have.property('color', 'black'); + should(number).have.property('number', 5515798402); + }); + done(); + }); + }); + it('stores the material', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + }).end(err => { + if (err) return done (err); + MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, data: any) => { + if (err) return done (err); + console.log(data[0]); + should(data[0]).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'Crastin CE 2510'); + should(data[0]).have.property('supplier', 'Du Pont'); + should(data[0]).have.property('group', 'PBT'); + should(data[0]).have.property('mineral', '0'); + should(data[0]).have.property('glass_fiber', '30'); + should(data[0]).have.property('carbon_fiber', '0'); + should(data[0].numbers).have.lengthOf(0); + done(); + }); + }); + }); + it('rejects already existing material names', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]}, + res: {status: 'Material name already taken'} + }); + }); + it('rejects wrong material properties', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 'x', glass_fiber: 'x', carbon_fiber: 'x', numbers: [{colorxx: 'black', number: 'xxx'}]}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects incomplete material properties', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'user'}, + httpStatus: 403, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + httpStatus: 401, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + }); + }); + }); +}); \ No newline at end of file diff --git a/src/routes/material.ts b/src/routes/material.ts new file mode 100644 index 0000000..42911e3 --- /dev/null +++ b/src/routes/material.ts @@ -0,0 +1,59 @@ +import express from 'express'; + +import MaterialValidate from './validate/material'; +import MaterialModel from '../models/material' +import IdValidate from './validate/id'; + + +const router = express.Router(); + +router.get('/materials', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + + MaterialModel.find({}).lean().exec((err, data) => { + if(err) next(err); + res.json(data.map(e => MaterialValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }); +}); + +router.get('/material/' + IdValidate.parameter(), (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + + MaterialModel.findById(req.params.id).lean().exec((err, data) => { + if(err) next(err); + console.log(data); + if (data) { + res.json(MaterialValidate.output(data)); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + +router.post('/material/new', (req, res, next) => { + if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + + // validate input + const {error, value: material} = MaterialValidate.input(req.body, 'new'); + if(error !== undefined) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + MaterialModel.find({name: material.name}).lean().exec((err, data) => { + if(err) next(err); + if (data.length > 0) { + res.status(400).json({status: 'Material name already taken'}); + return; + } + + new MaterialModel(material).save((err, data) => { + if(err) next(err); + res.json(MaterialValidate.output(data.toObject())); + }); + }) +}) + + +module.exports = router; \ No newline at end of file diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index 8148d2c..b103ef7 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -48,6 +48,13 @@ describe('/user', () => { httpStatus: 401 }); }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + httpStatus: 401 + }); + }); }); describe('GET /user/{name}', () => { @@ -119,6 +126,13 @@ describe('/user', () => { httpStatus: 404 }); }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/janedoe', + httpStatus: 401 + }); + }); }); describe('PUT /user/{name}', () => { @@ -169,7 +183,7 @@ describe('/user', () => { req: {name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', device_name: 'test'} }).end(err => { if (err) return done (err); - UserModel.find({name: 'adminnew'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'adminnew'}).lean().exec( (err, data) => { if (err) return done(err); should(data).have.lengthOf(1); should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); @@ -193,7 +207,7 @@ describe('/user', () => { req: {level: 'read'} }).end(err => { if (err) return done (err); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { if (err) return done(err); should(data).have.lengthOf(1); should(data[0]).have.property('level', 'read'); @@ -211,7 +225,7 @@ describe('/user', () => { }).end((err, res) => { if (err) return done (err); should(res.body).be.eql({status: 'Invalid body format'}); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { if (err) return done(err); should(data).have.lengthOf(1); should(data[0]).have.property('level', 'write'); @@ -229,7 +243,7 @@ describe('/user', () => { }).end((err, res) => { if (err) return done (err); should(res.body).be.eql({status: 'Username already taken'}); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { if (err) return done(err); should(data).have.lengthOf(1); done(); @@ -312,6 +326,14 @@ describe('/user', () => { req: {} }); }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + httpStatus: 401, + req: {} + }); + }); }); describe('DELETE /user/{name}', () => { @@ -324,7 +346,7 @@ describe('/user', () => { }).end((err, res) => { if (err) return done (err); should(res.body).be.eql({status: 'OK'}); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { if (err) return done(err); should(data).have.lengthOf(0); done(); @@ -340,7 +362,7 @@ describe('/user', () => { }).end((err, res) => { if (err) return done (err); should(res.body).be.eql({status: 'OK'}); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { if (err) return done(err); should(data).have.lengthOf(0); done(); @@ -379,6 +401,40 @@ describe('/user', () => { httpStatus: 404 }); }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/janedoe', + httpStatus: 401 + }); + }); + }); + + describe('GET /user/key', () => { + it('returns the right API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {key: TestHelper.auth.janedoe.key} + }); + }); + it('rejects requests from an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects requests from an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + httpStatus: 401 + }); + }); }); describe('POST /user/new', () => { @@ -410,7 +466,7 @@ describe('/user', () => { req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} }).end(err => { if (err) return done (err); - UserModel.find({name: 'johndoe'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'johndoe'}).lean().exec( (err, data) => { if (err) return done(err); should(data).have.lengthOf(1); should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); @@ -435,7 +491,7 @@ describe('/user', () => { }).end((err, res) => { if (err) return done (err); should(res.body).be.eql({status: 'Username already taken'}); - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { if (err) return done(err); should(data).have.lengthOf(1); done(); @@ -510,6 +566,14 @@ describe('/user', () => { req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} }); }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + httpStatus: 401, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }); + }); }); describe('POST /user/passreset', () => { @@ -539,7 +603,7 @@ describe('/user', () => { }); }); it('changes the user password', done => { - UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data: any) => { + UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { if (err) return done(err); const oldpass = data[0].pass; TestHelper.request(server, done, { @@ -559,24 +623,4 @@ describe('/user', () => { }); }); }); - - describe('GET /user/key', () => { - it('returns the right API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user/key', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {key: TestHelper.auth.janedoe.key} - }); - }); - it('rejects requests from an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user/key', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - }) }); \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts index f4326ea..aabb8a2 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -1,12 +1,14 @@ import express from 'express'; import mongoose from 'mongoose'; import bcrypt from 'bcryptjs'; + import UserValidate from './validate/user'; import UserModel from '../models/user'; import mail from '../helpers/mail'; const router = express.Router(); + router.get('/users', (req, res) => { if (!req.auth(res, ['admin'], 'basic')) return; @@ -168,4 +170,5 @@ router.post('/user/passreset', (req, res, next) => { }); }); + module.exports = router; \ No newline at end of file diff --git a/src/routes/validate/id.ts b/src/routes/validate/id.ts new file mode 100644 index 0000000..84024e9 --- /dev/null +++ b/src/routes/validate/id.ts @@ -0,0 +1,17 @@ +import joi from '@hapi/joi'; + +export default class IdValidate { + private static id = joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24); + + static get () { + return this.id; + } + + static valid (id) { + return this.id.validate(id).error === undefined; + } + + static parameter() { // :id url parameter + return ':id([0-9a-f]{24})'; + } +} \ No newline at end of file diff --git a/src/routes/validate/material.ts b/src/routes/validate/material.ts new file mode 100644 index 0000000..c5ac005 --- /dev/null +++ b/src/routes/validate/material.ts @@ -0,0 +1,82 @@ +import joi from '@hapi/joi'; + +import IdValidate from './id'; + +export default class MaterialValidate { // validate input for material + private static material = { + name: joi.string() + .max(128), + + supplier: joi.string() + .max(128), + + group: joi.string() + .max(128), + + mineral: joi.number() + .integer() + .min(0) + .max(100), + + glass_fiber: joi.number() + .integer() + .min(0) + .max(100), + + carbon_fiber: joi.number() + .integer() + .min(0) + .max(100), + + numbers: joi.array() + .items(joi.object({ + color: joi.string() + .max(128), + number: joi.number() + .min(0) + })) + }; + + static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated) + if (param === 'new') { + return joi.object({ + name: this.material.name.required(), + supplier: this.material.supplier.required(), + group: this.material.group.required(), + mineral: this.material.mineral.required(), + glass_fiber: this.material.glass_fiber.required(), + carbon_fiber: this.material.carbon_fiber.required(), + numbers: this.material.numbers + }).validate(data); + } + else if (param === 'change') { + return joi.object({ + name: this.material.name, + supplier: this.material.supplier, + group: this.material.group, + mineral: this.material.mineral, + glass_fiber: this.material.glass_fiber, + carbon_fiber: this.material.carbon_fiber, + numbers: this.material.numbers + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } + + static output (data) { // validate output from database for needed properties, strip everything else + data._id = data._id.toString(); + const {value, error} = joi.object({ + _id: IdValidate.get(), + name: this.material.name, + supplier: this.material.supplier, + group: this.material.group, + mineral: this.material.mineral, + glass_fiber: this.material.glass_fiber, + carbon_fiber: this.material.carbon_fiber, + numbers: this.material.numbers + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} \ No newline at end of file diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index 4f563c6..4b1259a 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -1,28 +1,34 @@ import joi from '@hapi/joi'; import globals from '../../globals'; +import IdValidate from './id'; + export default class UserValidate { // validate input for user private static user = { - _id: joi.any(), name: joi.string() .alphanum() - .lowercase(), + .lowercase() + .max(128), email: joi.string() .email({minDomainSegments: 2}) - .lowercase(), + .lowercase() + .max(128), pass: joi.string() - .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')), + .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')) + .max(128), level: joi.string() .valid(...globals.levels), location: joi.string() - .alphanum(), + .alphanum() + .max(128), device_name: joi.string() .allow('') + .max(128), }; private static specialUsernames = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take @@ -63,14 +69,15 @@ export default class UserValidate { // validate input for user } static output (data) { // validate output from database for needed properties, strip everything else + data._id = data._id.toString(); const {value, error} = joi.object({ - _id: joi.any(), - name: joi.string(), - email: joi.string(), - level: joi.string(), - location: joi.string(), - device_name: joi.string().allow('') - }).validate(data, {stripUnknown: true}) + _id: IdValidate.get(), + name: this.user.name, + email: this.user.email, + level: this.user.level, + location: this.user.location, + device_name: this.user.device_name + }).validate(data, {stripUnknown: true}); return error !== undefined? null : value; } diff --git a/src/test/db.json b/src/test/db.json index 4c2c645..0b4fd2f 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -2,27 +2,88 @@ "collections": { "users": [ { - "_id": {"$oid":"5ea0450ed851c30a90e70894"}, + "_id": {"$oid":"000000000000000000000001"}, + "email": "user@bosch.com", + "name": "user", + "pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi", + "level": "read", + "location": "Rng", + "device_name": "Alpha I", + "key": "000000000000000000001001", + "__v": 0 + }, + { + "_id": {"$oid":"000000000000000000000002"}, "email": "jane.doe@bosch.com", "name": "janedoe", "pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi", "level": "write", "location": "Rng", "device_name": "Alpha I", - "key": "5ea0450ed851c30a90e70899", + "key": "000000000000000000001002", "__v": 0 }, { - "_id": {"$oid":"5ea131671feb9c2ee0aafc9b"}, + "_id": {"$oid":"000000000000000000000003"}, "email": "a.d.m.i.n@bosch.com", "name": "admin", "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K", "level": "admin", "location": "Rng", "device_name": "", - "key": "5ea131671feb9c2ee0aafc9a", + "key": "000000000000000000001003", "__v": "0" } + ], + "materials": [ + { + "_id": {"$oid":"100000000000000000000001"}, + "name": "Stanyl TW 200 F8", + "supplier": "DSM", + "group": "PA46", + "mineral": 0, + "glass_fiber": 40, + "carbon_fiber": 0, + "numbers": [ + { + "color": "black", + "number": 5514263423 + } + ], + "__v": 0 + }, + { + "_id": {"$oid":"100000000000000000000002"}, + "name": "Ultramid T KR 4355 G7", + "supplier": "BASF", + "group": "PA6/6T", + "mineral": 0, + "glass_fiber": 35, + "carbon_fiber": 0, + "numbers": [ + { + "color": "black", + "number": 5514212901 + }, + { + "color": "signalviolet", + "number": 5514612901 + } + ], + "__v": 0 + }, + { + "_id": {"$oid":"100000000000000000000003"}, + "name": "PA GF 50 black (2706)", + "supplier": "Akro-Plastic", + "group": "PA66+PA6I/6T", + "mineral": 0, + "glass_fiber": 0, + "carbon_fiber": 0, + "numbers": [ + ], + "__v": 0 + } ] } } \ No newline at end of file diff --git a/static/styles/swagger.css b/static/styles/swagger.css new file mode 100644 index 0000000..e69de29 From 2f0d2f2276168c0137cd369c8d39dce837b1508d Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Wed, 29 Apr 2020 15:07:07 +0200 Subject: [PATCH 13/16] styled swagger --- src/index.ts | 5 +- static/img/bosch-logo.svg | 201 ++++++++++++++++++++++++++++ static/styles/swagger.css | 271 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 static/img/bosch-logo.svg diff --git a/src/index.ts b/src/index.ts index e8cf691..f79a554 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,9 @@ app.use('/', require('./routes/root')); app.use('/', require('./routes/user')); app.use('/', require('./routes/material')); +// static files +app.use('/static', express.static('static')); + // Swagger UI let apiDoc: JSONSchema = {}; jsonRefParser.bundle('api/api.yaml', (err, doc) => { @@ -56,7 +59,7 @@ jsonRefParser.bundle('api/api.yaml', (err, doc) => { apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); swagger.setup(apiDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'}); }); -app.use('/api', swagger.serve, swagger.setup(apiDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'})); +app.use('/api', swagger.serve, swagger.setup(apiDoc, {customCssUrl: '/static/styles/swagger.css'})); app.use((req, res) => { // 404 error handling res.status(404).json({status: 'Not found'}); diff --git a/static/img/bosch-logo.svg b/static/img/bosch-logo.svg new file mode 100644 index 0000000..fae963f --- /dev/null +++ b/static/img/bosch-logo.svg @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/styles/swagger.css b/static/styles/swagger.css index e69de29..b86a643 100644 --- a/static/styles/swagger.css +++ b/static/styles/swagger.css @@ -0,0 +1,271 @@ +/*Bosch styling for swagger*/ + +/*GET: dark blue*/ +/*POST: dark green*/ +/*PUT: turquoise*/ +/*DELETE: fuchsia*/ + +:root { + --red: #ea0016; + --dark-blue: #005691; + --dark-blue-w75: #bfd5e3; + --dark-green: #006249; + --dark-green-w75: #bfd8d1; + --turquoise: #00a8b0; + --turquoise-w75: #bfe9eb; + --fuchsia: #b90276; + --fuchsia-w75: #edc0dd; + --light-grey: #bfc0c2; + --light-grey-w75: #efeff0; + --light-green: #78be20; +} + +body { + background: #fff; +} + +body:before { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 16px; + content: ''; + background-repeat: no-repeat; + background-size: cover; + background-image: url(data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB4bWw6c3BhY2U9InByZXNlcnZlIgogIGhlaWdodD0iMzAwIgogIHdpZHRoPSI3MjAiCiAgdmVyc2lvbj0iMS4xIgogIHk9IjAiCiAgeD0iMCIKICB2aWV3Qm94PSIwIDAgNzIwIDMwMCI+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgIC5zdDAgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF8xXyIpOwogICAgfQogICAgLnN0MSB7CiAgICAgIGZpbGw6IHVybCgiI1NWR0lEXzJfIik7CiAgICB9CiAgICAuc3QyIHsKICAgICAgZmlsbDogdXJsKCIjU1ZHSURfM18iKTsKICAgIH0KICAgIC5zdDMgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF80XyIpOwogICAgfQogICAgLnN0NCB7CiAgICAgIGZpbGw6IHVybCgiI1NWR0lEXzVfIik7CiAgICB9CiAgICAuc3Q1IHsKICAgICAgZmlsbDogI0FGMjAyNDsKICAgIH0KICAgIC5zdDYgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF82XyIpOwogICAgfQogICAgLnN0NyB7CiAgICAgIGZpbGw6ICM5NDFCMUU7CiAgICB9CiAgICAuc3Q4IHsKICAgICAgZmlsbDogI0IxMjczOTsKICAgIH0KICAgIC5zdDkgewogICAgICBmaWxsOiAjOTUyNDMyOwogICAgfQogICAgLnN0MTAgewogICAgICBmaWxsOiAjRDQyMDI3OwogICAgfQogICAgLnN0MTEgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF83XyIpOwogICAgfQogICAgLnN0MTIgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF84XyIpOwogICAgfQogICAgLnN0MTMgewogICAgICBmaWxsOiAjMUM5QTQ4OwogICAgfQogICAgLnN0MTQgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF85XyIpOwogICAgfQogICAgLnN0MTUgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF8xMF8iKTsKICAgIH0KICAgIC5zdDE2IHsKICAgICAgZmlsbDogIzJBMzg4NjsKICAgIH0KICAgIC5zdDE3IHsKICAgICAgZmlsbDogdXJsKCIjU1ZHSURfMTFfIik7CiAgICB9CiAgICAuc3QxOCB7CiAgICAgIGZpbGw6IHVybCgiI1NWR0lEXzEyXyIpOwogICAgfQogICAgLnN0MTkgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF8xM18iKTsKICAgIH0KICAgIC5zdDIwIHsKICAgICAgZmlsbDogdXJsKCIjU1ZHSURfMTRfIik7CiAgICB9CiAgPC9zdHlsZT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMS41NSwtMy4zKSI+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzFfIiB5Mj0iLTMyLjY2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHkxPSItMzIuNjYzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9Ijg0Mi4wOCIgeDE9IjExOC45OCI+PHN0b3Agc3RvcC1jb2xvcj0iIzk1MjMzMSIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzkyMUMxRCIgb2Zmc2V0PSIuMDM2MDk0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iI0IwMjczOSIgb2Zmc2V0PSIuMDg0NjQ5Ii8+PHN0b3Agc3RvcC1jb2xvcj0iI0FEMUYyNCIgb2Zmc2V0PSIuMTIzNyIvPjxzdG9wIHN0b3AtY29sb3I9IiNDNzIwMjYiIG9mZnNldD0iLjE1MDkiLz48c3RvcCBzdG9wLWNvbG9yPSIjRDQyMDI3IiBvZmZzZXQ9Ii4xNjk3Ii8+PHN0b3Agc3RvcC1jb2xvcj0iI0NDMjQzMSIgb2Zmc2V0PSIuMTc1OCIvPjxzdG9wIHN0b3AtY29sb3I9IiNCNzJCNEMiIG9mZnNldD0iLjE4ODgiLz48c3RvcCBzdG9wLWNvbG9yPSIjOTUzMzcxIiBvZmZzZXQ9Ii4yMDc0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzg4MzU3RiIgb2Zmc2V0PSIuMjE0MiIvPjxzdG9wIHN0b3AtY29sb3I9IiM4NTM2ODEiIG9mZnNldD0iLjI0MzYiLz48c3RvcCBzdG9wLWNvbG9yPSIjNkYzNjhCIiBvZmZzZXQ9Ii4yNjM4Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzM5NDI4RiIgb2Zmc2V0PSIuMjkxMSIvPjxzdG9wIHN0b3AtY29sb3I9IiMyMzNEN0QiIG9mZnNldD0iLjMyNDIiLz48c3RvcCBzdG9wLWNvbG9yPSIjMzIyQzZGIiBvZmZzZXQ9Ii40MTgxIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzJBMzg4NSIgb2Zmc2V0PSIuNDk0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzFENjJBMSIgb2Zmc2V0PSIuNTU4MSIvPjxzdG9wIHN0b3AtY29sb3I9IiMyNzZDQTUiIG9mZnNldD0iLjU3MDIiLz48c3RvcCBzdG9wLWNvbG9yPSIjNDM4RUIzIiBvZmZzZXQ9Ii42MTAzIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzU1QTVCQyIgb2Zmc2V0PSIuNjM5OSIvPjxzdG9wIHN0b3AtY29sb3I9IiM1Q0FGQkYiIG9mZnNldD0iLjY1NTYiLz48c3RvcCBzdG9wLWNvbG9yPSIjNTZBQkJEIiBvZmZzZXQ9Ii42Nzc3Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzQzOUZCOCIgb2Zmc2V0PSIuNzA1OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMxODhFQUYiIG9mZnNldD0iLjczNzIiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDM4QkFFIiBvZmZzZXQ9Ii43NDI2Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzA2OTI5MiIgb2Zmc2V0PSIuNzg5OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMwNUExNEIiIG9mZnNldD0iLjg4NzUiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDM5MjdFIiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxyZWN0IHdpZHRoPSI3MjMuMSIgeT0iMCIgeD0iMCIgaGVpZ2h0PSIzMDYuNCIgY2xhc3M9InN0MCIgZmlsbD0idXJsKCNTVkdJRF8xXykiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iU1ZHSURfMl8iIHkyPSItMTA5LjI2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9Ii0xMDkuMjYiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgLTExOC45OCAxMjAuNTQpIiB4Mj0iMjM1Ljk4IiB4MT0iMzI1LjA4Ij48c3RvcCBzdG9wLWNvbG9yPSIjODkzNjgwIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjODkzNjgwIiBvZmZzZXQ9Ii4zMzU0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzhEMzE2RCIgb2Zmc2V0PSIuNTAyNSIvPjxzdG9wIHN0b3AtY29sb3I9IiM5MDI5NEQiIG9mZnNldD0iLjgzOTgiLz48c3RvcCBzdG9wLWNvbG9yPSIjOTAyNTQxIiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50Pjxwb2x5Z29uIHBvaW50cz0iMTc1LjEgMTUzLjIgMTE3IDMwNi40IDIwNi4xIDMwNi40IiBmaWxsPSJ1cmwoI1NWR0lEXzJfKSIgY2xhc3M9InN0MSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8zXyIgeTI9Ii04Mi4yODQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjI0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjQ0Ni41NSIgeDE9IjQ3OC45MyI+PHN0b3Agc3RvcC1jb2xvcj0iIzMyMkM2RiIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzMyMkM2RiIgb2Zmc2V0PSIuMjQyNyIvPjxzdG9wIHN0b3AtY29sb3I9IiMzMDJGNzIiIG9mZnNldD0iLjQ1OTkiLz48c3RvcCBzdG9wLWNvbG9yPSIjMkEzQTdFIiBvZmZzZXQ9Ii43MTU1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzE1NEE5MyIgb2Zmc2V0PSIuOTg5NiIvPjxzdG9wIHN0b3AtY29sb3I9IiMxMzRCOTQiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBvbHlnb24gcG9pbnRzPSIyODguNCAxNTMuMiAzMTAuNyAzMDYuNCAzNTguMSAzMDYuNCAzNTguMSAwIDMxMi45IDAiIGZpbGw9InVybCgjU1ZHSURfM18pIiBjbGFzcz0ic3QyIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzRfIiB5Mj0iLTMyLjY2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHkxPSItMzIuNjYzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjM3Mi44OCIgeDE9IjI5NC4wOCI+PHN0b3Agc3RvcC1jb2xvcj0iIzZGMzc4RCIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzNBNDI5MSIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cG9seWdvbiBwb2ludHM9IjE3NS4xIDE1My4yIDIwNi4xIDMwNi40IDI1My45IDE1My4yIDIwOS40IDAgMjA5LjQgMCIgZmlsbD0idXJsKCNTVkdJRF80XykiIGNsYXNzPSJzdDMiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iU1ZHSURfNV8iIHkyPSItMzIuNjYzIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9Ii0zMi42NjMiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgLTExOC45OCAxMjAuNTQpIiB4Mj0iMzI1LjA4IiB4MT0iNDMxLjg4Ij48c3RvcCBzdG9wLWNvbG9yPSIjMjMzRDdEIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjMjkzRDdEIiBvZmZzZXQ9Ii4yNDk1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzNBM0M4MCIgb2Zmc2V0PSIuNTQ0NiIvPjxzdG9wIHN0b3AtY29sb3I9IiM1MTNCODQiIG9mZnNldD0iLjg2MTYiLz48c3RvcCBzdG9wLWNvbG9yPSIjNUQzQTg2IiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50Pjxwb2x5Z29uIHBvaW50cz0iMjUzLjkgMTUzLjIgMjA2LjEgMzA2LjQgMzEwLjcgMzA2LjQgMjg4LjQgMTUzLjIgMzEyLjkgMCAyMDkuNCAwIiBmaWxsPSJ1cmwoI1NWR0lEXzVfKSIgY2xhc3M9InN0NCIvPjxwb2x5Z29uIHBvaW50cz0iMTE2LjEgMCA1NS43IDAgNTUuNyA5NC44IDg5LjkgMTUzLjIgNTUuNyAyMTEuNiA1NS43IDMwNi40IDExNyAzMDYuNCA5NS4yIDE1My4yIiBmaWxsPSIjYWYyMDI0IiBjbGFzcz0ic3Q1Ii8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzZfIiB5Mj0iNDMuOTM3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9IjQzLjkzNyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMTE4Ljk4IDEyMC41NCkiIHgyPSIyMzIuNjciIHgxPSIzMjkuMTEiPjxzdG9wIHN0b3AtY29sb3I9IiM4OTM2ODAiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiM4OTM2ODAiIG9mZnNldD0iLjMzNTQiLz48c3RvcCBzdG9wLWNvbG9yPSIjOEQzMTZEIiBvZmZzZXQ9Ii41MDI1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzkwMjk0RCIgb2Zmc2V0PSIuODM5OCIvPjxzdG9wIHN0b3AtY29sb3I9IiM5MDI1NDEiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBvbHlnb24gcG9pbnRzPSIxNzUuMSAxNTMuMiAyMDkuNCAwIDExNi4xIDAiIGZpbGw9InVybCgjU1ZHSURfNl8pIiBjbGFzcz0ic3Q2Ii8+PHBvbHlnb24gcG9pbnRzPSI1NS43IDk0LjggNTUuNyAwIDAgMCIgZmlsbD0iIzk0MWIxZSIgY2xhc3M9InN0NyIvPjxwb2x5Z29uIHBvaW50cz0iNTUuNyAyMTEuNiA4OS45IDE1My4yIDU1LjcgOTQuOCIgZmlsbD0iI2IxMjczOSIgY2xhc3M9InN0OCIvPjxwb2x5Z29uIHBvaW50cz0iNTUuNyAyMTEuNiAwIDMwNi40IDU1LjcgMzA2LjQiIGZpbGw9IiM5NDFiMWUiIGNsYXNzPSJzdDciLz48cG9seWdvbiBwb2ludHM9IjU1LjcgOTQuOCAwIDAgMCAzMDYuNCA1NS43IDIxMS42IiBmaWxsPSIjOTUyNDMyIiBjbGFzcz0ic3Q5Ii8+PHBvbHlnb24gcG9pbnRzPSIxMTYuMSAwIDk1LjIgMTUzLjIgMTE3IDMwNi40IDE3NS4xIDE1My4yIiBmaWxsPSIjZDQyMDI3IiBjbGFzcz0ic3QxMCIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF83XyIgeTI9Ii0xODYuMDYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjQ0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9Ijc0OC45NiIgeDE9Ijc0OC45NiI+PHN0b3Agc3RvcC1jb2xvcj0iIzk0QkU1NSIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzkzQkQ1OCIgb2Zmc2V0PSIuMDQ0MzQwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzhCQkM2QSIgb2Zmc2V0PSIuMzg5MSIvPjxzdG9wIHN0b3AtY29sb3I9IiM4NkJDNzUiIG9mZnNldD0iLjcxNDkiLz48c3RvcCBzdG9wLWNvbG9yPSIjODRCQzc5IiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxwYXRoCiAgICAgIGQ9Im02NDEuNiAyNTkuNmMxLjctMjUuNCAxMC01NC42IDE4LjgtODUuNiAxLjQtNSAyLjgtMTAgNC4yLTE1LjEtMS40LTUuNS0yLjgtMTAuOS00LjItMTYuMi04LjgtMzMuMy0xNy02NC43LTE4LjgtOTItMS40LTIxLjIgMS40LTM3IDguOS01MC42aC00NS45Yy03LjUgMTguMy0xMC4zIDI5LjEtOC45IDUwLjMgMS43IDI3LjMgMTAgNTguNyAxOC44IDkyIDEzIDQ5LjMgMjggMTA2LjIgMjMuMiAxNjQuMmgxMi45Yy03LjYtMTIuOC0xMC40LTI3LjMtOS00N3oiCiAgICAgIGNsYXNzPSJzdDExIgogICAgICBmaWxsPSJ1cmwoI1NWR0lEXzdfKSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF84XyIgeTI9Ii0xODQuNDUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTE3LjI5IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjczMy40OSIgeDE9IjY1My43NiI+PHN0b3Agc3RvcC1jb2xvcj0iIzA4QTI0QiIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzBBQTE0RSIgb2Zmc2V0PSIuMTY3OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMwQjlFNTciIG9mZnNldD0iLjQwNDciLz48c3RvcCBzdG9wLWNvbG9yPSIjMDk5QTY3IiBvZmZzZXQ9Ii42ODI3Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzA0OTQ3RCIgb2Zmc2V0PSIuOTg5OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMwNDkzN0UiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0ibTYxNC41IDE0Mi4zYy04LjgtMzMuMy0xNy02NC43LTE4LjgtOTItMS40LTIxLjIgMS40LTMyIDguOS01MC4zaC0zNS40YzUuNyA1My45LTMuOCAxMDYuNy0xMy42IDE2Ni44LTUuNyAzNS0xMS43IDcxLjMtMTMuMiAxMDAuNi0xLjEgMjEuMSAwLjQgMzIuOCAxLjggMzloOTMuNWM0LjgtNTcuOS0xMC4zLTExNC44LTIzLjItMTY0LjF6IiBjbGFzcz0ic3QxMiIgZmlsbD0idXJsKCNTVkdJRF84XykiLz48cGF0aCBjbGFzcz0ic3QxMyIgZmlsbD0iIzFjOWE0OCIgZD0ibTY2NC42IDE1OC45Yy0xLjQgNS4xLTIuOCAxMC4xLTQuMiAxNS4xLTguOCAzMS0xNyA2MC4yLTE4LjggODUuNi0xLjQgMTkuNyAxLjQgMzQuMiA5IDQ2LjloMzNjNC4yLTUxLjgtNy4yLTEwMi4zLTE5LTE0Ny42eiIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF85XyIgeTI9Ii0xODUuOTYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjU0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjgxMi44MyIgeDE9IjgxMi44MyI+PHN0b3Agc3RvcC1jb2xvcj0iIzY5QTA2MCIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzYzOUQ1QyIgb2Zmc2V0PSIuMDM5ODk1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzRDOTQ0RiIgb2Zmc2V0PSIuMjE5MiIvPjxzdG9wIHN0b3AtY29sb3I9IiMzNzhFNDciIG9mZnNldD0iLjQxODQiLz48c3RvcCBzdG9wLWNvbG9yPSIjMjk4QjQ0IiBvZmZzZXQ9Ii42NTE1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzIzOEE0MyIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJtNjgwLjUgMGMxMC43IDU1LjMtMi41IDExMC40LTE1LjkgMTU4LjkgMTEuNyA0NS4zIDIzLjIgOTUuOCAxOC45IDE0Ny42aDM5LjZ2LTMwNi41aC00Mi42eiIgY2xhc3M9InN0MTQiIGZpbGw9InVybCgjU1ZHSURfOV8pIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzEwXyIgeTI9Ii0xODUuODYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjU0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjY1Mi40NSIgeDE9IjY1Mi40NSI+PHN0b3Agc3RvcC1jb2xvcj0iIzA1QjVEQyIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzA0QjBENyIgb2Zmc2V0PSIuMjE5NyIvPjxzdG9wIHN0b3AtY29sb3I9IiMwNUE0QzkiIG9mZnNldD0iLjUzNzEiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDU5MUI0IiBvZmZzZXQ9Ii45MTIyIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzA1OENBRSIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJtNTQyLjMgMjY3LjRjMS41LTI5LjQgNy41LTY1LjYgMTMuMi0xMDAuNiA5LjgtNjAuMSAxOS4zLTExMi44IDEzLjYtMTY2LjhoLTcwLjhjLTEuNCAxMS40LTIuOSAxOS4yLTEuOCA0MS44IDEuNSAzMS42IDcuNSA3MC41IDEzLjIgMTA4LjIgOC40IDU1LjQgMTYuNiAxMDguOCAxNS4xIDE1Ni40aDE5LjJjLTEuMy02LjItMi44LTE3LjktMS43LTM5eiIgY2xhc3M9InN0MTUiIGZpbGw9InVybCgjU1ZHSURfMTBfKSIvPjxwb2x5Z29uIHBvaW50cz0iMzc1LjcgMTUzLjIgMzU4LjEgMCAzNTguMSAzMDYuNCIgZmlsbD0iIzJhMzg4NiIgY2xhc3M9InN0MTYiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iU1ZHSURfMTFfIiB5Mj0iNzcuMTM2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9Ii00LjMyODEiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgLTExOC45OCAxMjAuNTQpIiB4Mj0iNzk2LjcxIiB4MT0iNzUxLjA1Ij48c3RvcCBzdG9wLWNvbG9yPSIjNjJCMTZFIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjODdCOTU3IiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxwYXRoIGQ9Im02NDEuNiA1MC42YzEuNyAyNy4zIDEwIDU4LjcgMTguOCA5MiAxLjQgNS4zIDIuOCAxMC43IDQuMiAxNi4yIDEzLjUtNDguNCAyNi42LTEwMy41IDE1LjktMTU4LjhoLTMwYy03LjUgMTMuNi0xMC4zIDI5LjQtOC45IDUwLjZ6IiBjbGFzcz0ic3QxNyIgZmlsbD0idXJsKCNTVkdJRF8xMV8pIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzEyXyIgeTI9Ii0xODkuMjgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTEzLjcxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjYzMS41OSIgeDE9IjU1MC40Ij48c3RvcCBzdG9wLWNvbG9yPSIjMDY5QUQ0IiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjMzBBMENFIiBvZmZzZXQ9Ii4zNTI1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzVCQjBDMCIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJtNTA5LjggMTUwYy01LjctMzcuNy0xMS43LTc2LjYtMTMuMi0xMDguMi0xLjEtMjIuNyAwLjQtMzAuNCAxLjgtNDEuOGgtNDEuNWMxLjUgNDAuMS0xLjUgODUuMy03IDE2MC44LTMuMSA0My41LTggMTEwLjUtNyAxNDUuN2g4Mi4xYzEuNC00Ny43LTYuOC0xMDEuMS0xNS4yLTE1Ni41eiIgY2xhc3M9InN0MTgiIGZpbGw9InVybCgjU1ZHSURfMTJfKSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8xM18iIHkyPSItMTg1Ljg2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9IjEyMC41NCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMTE4Ljk4IDEyMC41NCkiIHgyPSI1MDUuMzMiIHgxPSI1MDUuMzMiPjxzdG9wIHN0b3AtY29sb3I9IiMxRTQ1OEUiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiMxRjRGOTYiIG9mZnNldD0iLjI0MTEiLz48c3RvcCBzdG9wLWNvbG9yPSIjMkI2QUFCIiBvZmZzZXQ9Ii43MjkyIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzMzN0JCOSIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cG9seWdvbiBwb2ludHM9IjM1OC4xIDMwNi40IDQxNC42IDMwNi40IDQxNC42IDAgMzU4LjEgMCAzNzUuNyAxNTMuMiIgZmlsbD0idXJsKCNTVkdJRF8xM18pIiBjbGFzcz0ic3QxOSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8xNF8iIHkyPSIxMjAuNTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iLTE4NS44NiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMTE4Ljk4IDEyMC41NCkiIHgyPSI1NTQuOTIiIHgxPSI1NTQuOTIiPjxzdG9wIHN0b3AtY29sb3I9IiMzRjlBQzkiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiMyMDYyQTIiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0ibTQ0OS45IDE2MC44YzUuNS03NS41IDguNS0xMjAuNiA3LTE2MC44aC00Mi4ybC0wLjEgMzA2LjRoMjguM2MtMS0zNS4xIDMuOC0xMDIuMSA3LTE0NS42eiIgY2xhc3M9InN0MjAiIGZpbGw9InVybCgjU1ZHSURfMTRfKSIvPjwvZz4KPC9zdmc+Cg==); +} + +body:after { + position: absolute; + right: 25px; + top: 36px; + width: 135px; + height: 48px; + content: ''; + background-repeat: no-repeat; + background-size: cover; + background-image: url(/static/img/bosch-logo.svg); +} + +.swagger-ui { + font-family: "Bosch Sans", sans-serif; +} + +/*Remove topbar*/ +.swagger-ui .topbar { + display: none +} + +/*Remove models view*/ +.swagger-ui .models { + display: none; +} + +/*Remove application/json select*/ +.swagger-ui .opblock .opblock-section-header > label, .swagger-ui .response-controls { + display: none; +} + +/*Remove border radius*/ +.swagger-ui .opblock, .swagger-ui .opblock .opblock-summary-method, .swagger-ui select { + border-radius: 0; + box-shadow: none; +} + +/*remove links in response*/ +.swagger-ui .response-col_links { + display: none; +} + +/*remove version*/ +.swagger-ui .info .title span { + display: none; +} + +/*separator before methods*/ +.swagger-ui .scheme-container { + box-shadow: none; + border-bottom: 1px solid var(--light-grey); +} + +/*tag separator*/ +.swagger-ui .opblock-tag { + border-bottom: 1px solid var(--light-grey); +} + +/*parameters/responses bar*/ +.swagger-ui .opblock .opblock-section-header { + box-shadow: none; + background: #fff; +} + +/*select*/ +.swagger-ui select { + background-color: var(--light-grey-w75); + border: none; + height: 36px; +} + +/*button*/ +.swagger-ui .btn { + border-radius: 0; + box-shadow: none; +} + +.swagger-ui .btn:hover { + box-shadow: none; +} + +/*authorize button */ +.swagger-ui .btn.authorize { + color: var(--light-green); + border-color: var(--light-green); +} + +.swagger-ui .btn.authorize svg { + fill: var(--light-green); +} + +/*auth inputs*/ +.swagger-ui .auth-container input[type="password"], .swagger-ui .auth-container input[type="text"] { + border-radius: 0; + box-shadow: none; + border-color: var(--light-grey); +} + +.swagger-ui .dialog-ux .modal-ux { + border-radius: 0; +} + +/*cancel button*/ +.swagger-ui .btn.cancel { + color: var(--red); + border-color: var(--red); +} + +/*model*/ +.swagger-ui .model-box { + border-radius: 0; +} + +/*execute button*/ +.swagger-ui .btn.execute { + background-color: var(--dark-blue); + border-color: var(--dark-blue); +} + +/*parameter input*/ +.swagger-ui .parameters-col_description input[type="text"] { + border-radius: 0; +} + +/*Remove colored parameters bar*/ +.swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after { + background: none; +} + +/*code*/ +.swagger-ui .opblock-body pre.microlight { + border-radius: 0; +} + +.swagger-ui .highlight-code > .microlight { + min-height: 0; +} + +/*request body*/ +.swagger-ui textarea { + border-radius: 0; +} + +/*parameters smaller padding*/ +.swagger-ui .execute-wrapper { + padding-top: 0; + padding-bottom: 0; +} + +.swagger-ui .btn.execute { + margin-bottom: 20px; +} + +.swagger-ui .opblock-description-wrapper { + margin-top: 20px; +} + +.swagger-ui .opblock-description-wrapper { + margin-top: 5px; +} + +/*response element positions*/ +.swagger-ui .model-example { + position: relative; + margin-top: 0; +} + +.swagger-ui .tab { + position: absolute; + top: -35px; + right: 0; +} + +.swagger-ui table tbody tr td { + padding: 0; +} + +.swagger-ui .renderedMarkdown p { + margin: 8px auto; +} + +/*Method colors*/ +.swagger-ui .opblock.opblock-get .opblock-summary-method { + background: var(--dark-blue); +} + +.swagger-ui .opblock.opblock-get .opblock-summary { + border-color: var(--dark-blue); +} + +.swagger-ui .opblock.opblock-get { + background: var(--dark-blue-w75); + border-color: var(--dark-blue); +} + +.swagger-ui .opblock.opblock-post .opblock-summary-method { + background: var(--dark-green); +} + +.swagger-ui .opblock.opblock-post .opblock-summary { + border-color: var(--dark-green); +} + +.swagger-ui .opblock.opblock-post { + background: var(--dark-green-w75); + border-color: var(--dark-green); +} + +.swagger-ui .opblock.opblock-put .opblock-summary-method { + background: var(--turquoise); +} + +.swagger-ui .opblock.opblock-put .opblock-summary { + border-color: var(--turquoise); +} + +.swagger-ui .opblock.opblock-put { + background: var(--turquoise-w75); + border-color: var(--turquoise); +} + +.swagger-ui .opblock.opblock-delete .opblock-summary-method { + background: var(--fuchsia); +} + +.swagger-ui .opblock.opblock-delete .opblock-summary { + border-color: var(--fuchsia); +} + +.swagger-ui .opblock.opblock-delete { + background: var(--fuchsia-w75); + border-color: var(--fuchsia); +} \ No newline at end of file From 5f20afcf0415e4312e59ff030bc19b866399534e Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Wed, 29 Apr 2020 16:09:31 +0200 Subject: [PATCH 14/16] finished /material methods --- api/material.yaml | 4 +-- src/routes/material.spec.ts | 26 +++++++++++------ src/routes/material.ts | 56 +++++++++++++++++++++++++++++++++++-- src/routes/user.ts | 2 +- static/styles/swagger.css | 4 +++ 5 files changed, 78 insertions(+), 14 deletions(-) diff --git a/api/material.yaml b/api/material.yaml index c32c1ab..8e3a039 100644 --- a/api/material.yaml +++ b/api/material.yaml @@ -40,7 +40,7 @@ 500: $ref: 'api.yaml#/components/responses/500' put: - summary: TODO change material + summary: change material description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /material @@ -79,8 +79,6 @@ responses: 200: $ref: 'api.yaml#/components/responses/Ok' - 400: - $ref: 'api.yaml#/components/responses/400' 401: $ref: 'api.yaml#/components/responses/401' 403: diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts index bb82bc1..e98da67 100644 --- a/src/routes/material.spec.ts +++ b/src/routes/material.spec.ts @@ -130,6 +130,16 @@ describe('/material', () => { res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]} }); }); + it('returns keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]}, + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]} + }); + }); it('changes the given properties', done => { TestHelper.request(server, done, { method: 'put', @@ -140,9 +150,11 @@ describe('/material', () => { , }).end(err => { if (err) return done(err); - MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { + MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => { if (err) return done(err); - should(data).be.eql({_id: '100000000000000000000002', name: 'UltramidTKR4355G7', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}], __v: 0} + data._id = data._id.toString(); + data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}}); + should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: '0', glass_fiber: '35', carbon_fiber: '0', numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}], __v: 0} ); done(); }); @@ -154,7 +166,7 @@ describe('/material', () => { url: '/material/100000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {name: 'UltramidTKR4355G7'}, + req: {name: 'Ultramid T KR 4355 G7'}, res: {status: 'Material name already taken'} }); }); @@ -173,9 +185,8 @@ describe('/material', () => { method: 'put', url: '/material/10000000000000000000000x', auth: {basic: 'admin'}, - httpStatus: 400, + httpStatus: 404, req: {}, - res: {status: 'Invalid id'} }); }); it('rejects an API key', done => { @@ -227,7 +238,7 @@ describe('/material', () => { should(res.body).be.eql({status: 'OK'}); MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { if (err) return done(err); - should(data).have.lengthOf(0); + should(data).be.null(); done(); }); }); @@ -238,8 +249,7 @@ describe('/material', () => { method: 'delete', url: '/material/10000000000000000000000x', auth: {basic: 'admin'}, - httpStatus: 400, - res: {status: 'Invalid id'} + httpStatus: 404 }); }); it('rejects an API key', done => { diff --git a/src/routes/material.ts b/src/routes/material.ts index 42911e3..f193b9f 100644 --- a/src/routes/material.ts +++ b/src/routes/material.ts @@ -31,6 +31,58 @@ router.get('/material/' + IdValidate.parameter(), (req, res, next) => { }); }); +router.put('/material/' + IdValidate.parameter(), (req, res, next) => { + if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + + const {error, value: material} = MaterialValidate.input(req.body, 'change'); + if(error !== undefined) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + if (material.hasOwnProperty('name')) { + MaterialModel.find({name: material.name}).lean().exec((err, data) => { + if(err) next(err); + if (data.length > 0 && data[0]._id != req.params.id) { + res.status(400).json({status: 'Material name already taken'}); + return; + } + else { + f(); + } + }); + } + else { + f(); + } + + function f() { // to resolve async + MaterialModel.findByIdAndUpdate(req.params.id, material).lean().exec((err, data) => { + if (err) next(err); + if (data) { + res.json(MaterialValidate.output(data)); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); + } +}); + +router.delete('/material/' + IdValidate.parameter(), (req, res, next) => { + if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + + MaterialModel.findByIdAndDelete(req.params.id).lean().exec((err, data) => { + if (err) next(err); + if (data) { + res.json({status: 'OK'}) + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + router.post('/material/new', (req, res, next) => { if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; @@ -52,8 +104,8 @@ router.post('/material/new', (req, res, next) => { if(err) next(err); res.json(MaterialValidate.output(data.toObject())); }); - }) -}) + }); +}); module.exports = router; \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts index aabb8a2..c60dd7b 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -67,7 +67,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => { if (err) next(err); if (data) { - res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + res.json(UserValidate.output(data)); } else { res.status(404).json({status: 'Not found'}); diff --git a/static/styles/swagger.css b/static/styles/swagger.css index b86a643..ac69d38 100644 --- a/static/styles/swagger.css +++ b/static/styles/swagger.css @@ -197,6 +197,10 @@ body:after { margin-top: 5px; } +.opblock-section .opblock-section-request-body > div > div { + padding-top: 18px; +} + /*response element positions*/ .swagger-ui .model-example { position: relative; From af071a9445e16534f845eb60d6171abd6b1e6815 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Mon, 4 May 2020 15:48:07 +0200 Subject: [PATCH 15/16] finished /template methods --- api/api.yaml | 2 +- api/material.yaml | 4 +- api/parameters.yaml | 1 + api/template.yaml | 61 +-- package.json | 2 +- src/db.ts | 2 + src/index.ts | 1 + src/models/measurement_template.ts | 11 + src/models/treatment_template.ts | 11 + src/routes/material.spec.ts | 7 +- src/routes/material.ts | 2 +- src/routes/template.spec.ts | 579 +++++++++++++++++++++++++++++ src/routes/template.ts | 91 +++++ src/routes/validate/template.ts | 59 +++ src/test/db.json | 66 ++++ static/styles/swagger.css | 31 ++ 16 files changed, 898 insertions(+), 32 deletions(-) create mode 100644 src/models/measurement_template.ts create mode 100644 src/models/treatment_template.ts create mode 100644 src/routes/template.spec.ts create mode 100644 src/routes/template.ts create mode 100644 src/routes/validate/template.ts diff --git a/api/api.yaml b/api/api.yaml index 6e25698..44756ae 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -48,7 +48,7 @@ tags: - name: /material - name: /condition - name: /measurement - - name: /templates + - name: /template - name: /model - name: /user diff --git a/api/material.yaml b/api/material.yaml index 8e3a039..a3b80da 100644 --- a/api/material.yaml +++ b/api/material.yaml @@ -70,7 +70,7 @@ 500: $ref: 'api.yaml#/components/responses/500' delete: - summary: TODO delete material + summary: delete material description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /material @@ -90,7 +90,7 @@ /material/new: post: - summary: TODO add material + summary: add material description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /material diff --git a/api/parameters.yaml b/api/parameters.yaml index f370c13..ba8d046 100644 --- a/api/parameters.yaml +++ b/api/parameters.yaml @@ -7,6 +7,7 @@ Id: example: 5ea0450ed851c30a90e70894 Name: name: name + description: has to be URL encoded in: path required: true schema: diff --git a/api/template.yaml b/api/template.yaml index 9219d81..5b362fb 100644 --- a/api/template.yaml +++ b/api/template.yaml @@ -1,9 +1,9 @@ /template/treatments: get: - summary: TODO all available treatment methods + summary: all available treatment methods description: 'Auth: basic, levels: read, write, maintain, dev, admin' tags: - - /templates + - /template security: - BasicAuth: [] responses: @@ -16,23 +16,26 @@ items: $ref: 'api.yaml#/components/schemas/Template' example: + _id: 5ea0450ed851c30a90e70894 name: heat aging parameters: - name: method range: - - copper + values: + - copper + - hot air 401: $ref: 'api.yaml#/components/responses/401' 500: $ref: 'api.yaml#/components/responses/500' -/templates/treatment/{name}: +/template/treatment/{name}: parameters: - $ref: 'api.yaml#/components/parameters/Name' get: - summary: TODO treatment method details + summary: treatment method details description: 'Auth: basic, levels: read, write, maintain, admin' tags: - - /templates + - /template security: - BasicAuth: [] responses: @@ -44,13 +47,14 @@ allOf: - $ref: 'api.yaml#/components/schemas/Template' example: + _id: 5ea0450ed851c30a90e70894 name: heat aging parameters: - name: method range: - - copper - 400: - $ref: 'api.yaml#/components/responses/400' + values: + - copper + - hot air 401: $ref: 'api.yaml#/components/responses/401' 404: @@ -58,10 +62,10 @@ 500: $ref: 'api.yaml#/components/responses/500' put: - summary: TODO add/change treatment method + summary: add/change treatment method description: 'Auth: basic, levels: maintain, admin' tags: - - /templates + - /template security: - BasicAuth: [] requestBody: @@ -76,7 +80,9 @@ parameters: - name: method range: - - copper + values: + - copper + - hot air responses: 200: description: treatment details @@ -86,11 +92,14 @@ allOf: - $ref: 'api.yaml#/components/schemas/Template' example: + _id: 5ea0450ed851c30a90e70894 name: heat aging parameters: - name: method range: - - copper + values: + - copper + - hot air 400: $ref: 'api.yaml#/components/responses/400' 401: @@ -102,10 +111,10 @@ 500: $ref: 'api.yaml#/components/responses/500' delete: - summary: TODO delete treatment method + summary: delete treatment method description: 'Auth: basic, levels: maintain, admin' tags: - - /templates + - /template security: - BasicAuth: [] responses: @@ -123,10 +132,10 @@ $ref: 'api.yaml#/components/responses/500' /template/measurements: get: - summary: TODO all available measurement methods + summary: all available measurement methods description: 'Auth: basic, levels: read, write, maintain, dev, admin' tags: - - /templates + - /template security: - BasicAuth: [] responses: @@ -139,6 +148,7 @@ items: $ref: 'api.yaml#/components/schemas/Template' example: + _id: 5ea0450ed851c30a90e70894 name: humidity parameters: - name: kf @@ -149,14 +159,14 @@ $ref: 'api.yaml#/components/responses/401' 500: $ref: 'api.yaml#/components/responses/500' -/templates/measurement/{name}: +/template/measurement/{name}: parameters: - $ref: 'api.yaml#/components/parameters/Name' get: - summary: TODO measurement method details + summary: measurement method details description: 'Auth: basic, levels: read, write, maintain, admin' tags: - - /templates + - /template security: - BasicAuth: [] responses: @@ -168,6 +178,7 @@ allOf: - $ref: 'api.yaml#/components/schemas/Template' example: + _id: 5ea0450ed851c30a90e70894 name: humidity parameters: - name: kf @@ -183,10 +194,10 @@ 500: $ref: 'api.yaml#/components/responses/500' put: - summary: TODO add/change measurement method + summary: add/change measurement method description: 'Auth: basic, levels: maintain, admin' tags: - - /templates + - /template security: - BasicAuth: [] requestBody: @@ -197,6 +208,7 @@ allOf: - $ref: 'api.yaml#/components/schemas/Template' example: + _id: 5ea0450ed851c30a90e70894 name: humidity parameters: - name: kf @@ -212,6 +224,7 @@ allOf: - $ref: 'api.yaml#/components/schemas/Template' example: + _id: 5ea0450ed851c30a90e70894 name: humidity parameters: - name: kf @@ -229,10 +242,10 @@ 500: $ref: 'api.yaml#/components/responses/500' delete: - summary: TODO delete measurement method + summary: delete measurement method description: 'Auth: basic, levels: maintain, admin' tags: - - /templates + - /template security: - BasicAuth: [] responses: diff --git a/package.json b/package.json index 4753647..d3f9e63 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "tsc": "tsc", "test": "mocha dist/**/**.spec.js", - "start": "tsc && node dist/index.js", + "start": "tsc && node dist/index.js || exit 1", "dev": "nodemon -e ts,yaml --exec \"npm run start\"" }, "keywords": [], diff --git a/src/db.ts b/src/db.ts index e624696..090e275 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,6 +1,8 @@ import mongoose from 'mongoose'; import cfenv from 'cfenv'; +// mongoose.set('debug', true); // enable mongoose debug + // database urls, prod db url is retrieved automatically const TESTING_URL = 'mongodb://localhost/dfopdb_test'; const DEV_URL = 'mongodb://localhost/dfopdb'; diff --git a/src/index.ts b/src/index.ts index f79a554..15bd504 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,6 +47,7 @@ app.use(require('./helpers/authorize')); // handle authentication app.use('/', require('./routes/root')); app.use('/', require('./routes/user')); app.use('/', require('./routes/material')); +app.use('/', require('./routes/template')); // static files app.use('/static', express.static('static')); diff --git a/src/models/measurement_template.ts b/src/models/measurement_template.ts new file mode 100644 index 0000000..c55cbc7 --- /dev/null +++ b/src/models/measurement_template.ts @@ -0,0 +1,11 @@ +import mongoose from 'mongoose'; + +const MeasurementTemplateSchema = new mongoose.Schema({ + name: {type: String, index: {unique: true}}, + parameters: [{ + name: String, + range: mongoose.Schema.Types.Mixed + }] +}, {minimize: false}); // to allow empty objects + +export default mongoose.model('measurement_template', MeasurementTemplateSchema); \ No newline at end of file diff --git a/src/models/treatment_template.ts b/src/models/treatment_template.ts new file mode 100644 index 0000000..3b61164 --- /dev/null +++ b/src/models/treatment_template.ts @@ -0,0 +1,11 @@ +import mongoose from 'mongoose'; + +const TreatmentTemplateSchema = new mongoose.Schema({ + name: {type: String, index: {unique: true}}, + parameters: [{ + name: String, + range: mongoose.Schema.Types.Mixed + }] +}, {minimize: false}); // to allow empty objects + +export default mongoose.model('treatment_template', TreatmentTemplateSchema); \ No newline at end of file diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts index e98da67..c69538a 100644 --- a/src/routes/material.spec.ts +++ b/src/routes/material.spec.ts @@ -130,7 +130,7 @@ describe('/material', () => { res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]} }); }); - it('returns keeps unchanged properties', done => { + it('keeps unchanged properties', done => { TestHelper.request(server, done, { method: 'put', url: '/material/100000000000000000000001', @@ -148,11 +148,12 @@ describe('/material', () => { httpStatus: 200, req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]} , - }).end(err => { + }).end((err, res) => { if (err) return done(err); + should(res.body).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]}); MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => { if (err) return done(err); - data._id = data._id.toString(); + data._id = data._id.toString({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]}); data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}}); should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: '0', glass_fiber: '35', carbon_fiber: '0', numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}], __v: 0} ); diff --git a/src/routes/material.ts b/src/routes/material.ts index f193b9f..c44afa7 100644 --- a/src/routes/material.ts +++ b/src/routes/material.ts @@ -57,7 +57,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => { } function f() { // to resolve async - MaterialModel.findByIdAndUpdate(req.params.id, material).lean().exec((err, data) => { + MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).lean().exec((err, data) => { if (err) next(err); if (data) { res.json(MaterialValidate.output(data)); diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts new file mode 100644 index 0000000..5ee4d1a --- /dev/null +++ b/src/routes/template.spec.ts @@ -0,0 +1,579 @@ +import should from 'should/as-function'; +import TemplateTreatmentModel from '../models/treatment_template'; +import TemplateMeasurementModel from '../models/measurement_template'; +import TestHelper from "../helpers/test"; + + +describe('/template', () => { + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + + describe('/template/treatment', () => { + describe('GET /template/treatments', () => { + it('returns all treatment templates', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatments', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.treatment_templates.length); + should(res.body).matchEach(treatment => { + should(treatment).have.only.keys('_id', 'name', 'parameters'); + should(treatment).have.property('_id').be.type('string'); + should(treatment).have.property('name').be.type('string'); + should(treatment.parameters).matchEach(number => { + should(number).have.only.keys('name', 'range'); + should(number).have.property('name').be.type('string'); + should(number).have.property('range').be.type('object'); + }); + }); + done(); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatments', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatments', + httpStatus: 401 + }); + }); + }); + + describe('GET /template/treatment/{name}', () => { + it('returns the right treatment template', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatment/heat%20treatment', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects an unknown name', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatment/xxx', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatment/heat%20treatment', + httpStatus: 401 + }); + }); + }); + + describe('PUT /template/treatment/{name}', () => { + it('returns the right treatment template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + }); + }); + it('keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + }); + }); + it('changes the given properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '200000000000000000000001', name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}); + TemplateTreatmentModel.find({name: 'heat aging'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'parameters'); + should(data[0]).have.property('name', 'heat aging'); + should(data[0]).have.property('parameters').have.lengthOf(1); + should(data[0].parameters[0]).have.property('name', 'time'); + should(data[0].parameters[0]).have.property('range'); + should(data[0].parameters[0].range).have.property('min', 1); + done(); + }); + }); + }); + it('supports values ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {values: [1, 2, 5]}}]}, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'time', range: {values: [1, 2, 5]}}]} + }); + }); + it('supports min max ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {min: 1, max: 11}}]}, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'time', range: {min: 1, max: 11}}]} + }); + }); + it('supports empty ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {}}]}, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'time', range: {}}]} + }); + }); + it('adds a new template for an unknown name', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20aging', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }).end(err => { + if (err) return done(err); + TemplateTreatmentModel.find({name: 'heat aging'}).lean().exec((err, data:any) => { + if (err) return done(err); + console.log(data); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'parameters', '__v'); + should(data[0]).have.property('name', 'heat aging'); + should(data[0].parameters[0]).have.property('name', 'time'); + should(data[0].parameters[0]).have.property('range'); + should(data[0].parameters[0].range).have.property('min', 1); + done(); + }); + }); + }); + it('rejects an incomplete template for a new name', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20aging', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'time'}]}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects already existing names', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat treatment 2', parameters: [{name: 'time', range: {min: 1}}]}, + res: {status: 'Template name already taken'} + }); + }); + it('rejects wrong properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20aging', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'time'}], xx: 33}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + httpStatus: 401, + req: {} + }); + }); + }); + + describe('DELETE /template/treatment/{name}', () => { + it('deletes the template', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + TemplateTreatmentModel.find({name: 'heat treatment'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('rejects deleting a template still in use'); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/treatment/heat%20treatment', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }) + }); + it('returns 404 for an unknown name', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/treatment/xxx', + auth: {basic: 'admin'}, + httpStatus: 404 + }) + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/treatment/heat%20treatment', + httpStatus: 401 + }) + }); + }); + }); + + describe('/template/measurement', () => { + describe('GET /template/measurements', () => { + it('returns all measurement templates', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurements', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.measurement_templates.length); + should(res.body).matchEach(measurement => { + should(measurement).have.only.keys('_id', 'name', 'parameters'); + should(measurement).have.property('_id').be.type('string'); + should(measurement).have.property('name').be.type('string'); + should(measurement.parameters).matchEach(number => { + should(number).have.only.keys('name', 'range'); + should(number).have.property('name').be.type('string'); + should(number).have.property('range').be.type('object'); + }); + }); + done(); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurements', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurements', + httpStatus: 401 + }); + }); + }); + + describe('GET /template/measurement/{name}', () => { + it('returns the right measurement template', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurement/spectrum', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurement/spectrum', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects an unknown name', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurement/xxx', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurement/spectrum', + httpStatus: 401 + }); + }); + }); + + describe('PUT /template/measurement/{name}', () => { + it('returns the right measurement template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]} + }); + }); + it('keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'spectrum', parameters: [{name: 'dpt', range: {}}]}, + res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]} + }); + }); + it('changes the given properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}, + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '300000000000000000000001', name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}); + TemplateMeasurementModel.find({name: 'IR spectrum'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'parameters'); + should(data[0]).have.property('name', 'IR spectrum'); + should(data[0]).have.property('parameters').have.lengthOf(1); + should(data[0].parameters[0]).have.property('name', 'data point table'); + should(data[0].parameters[0]).have.property('range'); + should(data[0].parameters[0].range).have.property('min', 0); + should(data[0].parameters[0].range).have.property('max', 1000); + done(); + }); + }); + }); + it('supports values ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]}, + res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]} + }); + }); + it('supports min max ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]}, + res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]} + }); + }); + it('supports empty ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/kf', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'weight %', range: {}}]}, + res: {_id: '300000000000000000000002', name: 'kf', parameters: [{name: 'weight %', range: {}}]} + }); + }); + it('adds a new template for an unknown name', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/vz', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]} + }).end(err => { + if (err) return done(err); + TemplateMeasurementModel.find({name: 'vz'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'parameters', '__v'); + should(data[0]).have.property('name', 'vz'); + should(data[0]).have.property('parameters').have.lengthOf(1); + should(data[0].parameters[0]).have.property('name', 'vz'); + should(data[0].parameters[0]).have.property('range'); + should(data[0].parameters[0].range).have.property('min', 1); + done(); + }); + }); + }); + it('rejects an incomplete template for a new name', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/vz', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'vz'}]}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects already existing names', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'kf', parameters: [{name: 'dpt', range: {min: 1}}]}, + res: {status: 'Template name already taken'} + }); + }); + it('rejects wrong properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'dpt'}], xx: 33}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + httpStatus: 401, + req: {} + }); + }); + }); + + describe('DELETE /template/measurement/{name}', () => { + it('deletes the template', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + TemplateMeasurementModel.find({name: 'spectrum'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('rejects deleting a template still in use'); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/measurement/spectrum', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/measurement/spectrum', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }) + }); + it('returns 404 for an unknown name', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/measurement/xxx', + auth: {basic: 'admin'}, + httpStatus: 404 + }) + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/measurement/spectrum', + httpStatus: 401 + }) + }); + }); + }); +}); \ No newline at end of file diff --git a/src/routes/template.ts b/src/routes/template.ts new file mode 100644 index 0000000..7e4aee7 --- /dev/null +++ b/src/routes/template.ts @@ -0,0 +1,91 @@ +import express from 'express'; + +import TemplateValidate from './validate/template'; +import TemplateTreatmentModel from '../models/treatment_template'; +import TemplateMeasurementModel from '../models/measurement_template'; + + +const router = express.Router(); + +router.get('/template/:collection(measurements|treatments)', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + + (req.params.collection === 'treatments' ? TemplateTreatmentModel : TemplateMeasurementModel) + .find({}).lean().exec((err, data) => { + if (err) next (err); + res.json(data.map(e => TemplateValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }); +}); + +router.get('/template/:collection(measurement|treatment)/:name', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + + (req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel) + .findOne({name: req.params.name}).lean().exec((err, data) => { + if (err) next (err); + if (data) { + res.json(TemplateValidate.output(data)); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + +router.put('/template/:collection(measurement|treatment)/:name', (req, res, next) => { + if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + + const collectionModel = req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel; + + collectionModel.findOne({name: req.params.name}).lean().exec((err, data) => { + if (err) next (err); + const templateState = data? 'change': 'new'; + const {error, value: template} = TemplateValidate.input(req.body, templateState); + if(error !== undefined) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + if (template.hasOwnProperty('name') && template.name !== req.params.name) { + collectionModel.find({name: template.name}).lean().exec((err, data) => { + if (err) next (err); + if (data.length > 0) { + res.status(400).json({status: 'Template name already taken'}); + return; + } + else { + f(); + } + }); + } + else { + f(); + } + + function f() { // to resolve async + collectionModel.findOneAndUpdate({name: req.params.name}, template, {new: true, upsert: true}).lean().exec((err, data) => { + if (err) next(err); + res.json(TemplateValidate.output(data)); + }); + } + }); +}); + +router.delete('/template/:collection(measurement|treatment)/:name', (req, res, next) => { + if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + + (req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel) + .findOneAndDelete({name: req.params.name}).lean().exec((err, data) => { + if (err) next(err); + if (data) { + res.json({status: 'OK'}) + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + + + +module.exports = router; \ No newline at end of file diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts new file mode 100644 index 0000000..6a0a23f --- /dev/null +++ b/src/routes/validate/template.ts @@ -0,0 +1,59 @@ +import joi from '@hapi/joi'; +import IdValidate from './id'; + +export default class TemplateValidate { + private static template = { + name: joi.string() + .max(128), + + parameters: joi.array() + .min(1) + .items( + joi.object({ + name: joi.string() + .max(128) + .required(), + + range: joi.object({ + values: joi.array() + .min(1), + + min: joi.number(), + + max: joi.number() + }) + .oxor('values', 'min') + .oxor('values', 'max') + .required() + }) + ) + }; + + static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated) + if (param === 'new') { + return joi.object({ + name: this.template.name.required(), + parameters: this.template.parameters.required() + }).validate(data); + } + else if (param === 'change') { + return joi.object({ + name: this.template.name, + parameters: this.template.parameters + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } + + static output (data) { // validate output from database for needed properties, strip everything else + data._id = data._id.toString(); + const {value, error} = joi.object({ + _id: IdValidate.get(), + name: this.template.name, + parameters: this.template.parameters + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} \ No newline at end of file diff --git a/src/test/db.json b/src/test/db.json index 0b4fd2f..d1bca35 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -84,6 +84,72 @@ ], "__v": 0 } + ], + "treatment_templates": [ + { + "_id": {"$oid":"200000000000000000000001"}, + "name": "heat treatment", + "parameters": [ + { + "name": "material", + "range": { + "values": [ + "copper", + "hot air" + ] + } + }, + { + "name": "weeks", + "range": { + "min": 1, + "max": 10 + } + } + ] + }, + { + "_id": {"$oid":"200000000000000000000002"}, + "name": "heat treatment 2", + "parameters": [ + { + "name": "material", + "range": {} + } + ] + } + ], + "measurement_templates": [ + { + "_id": {"$oid":"300000000000000000000001"}, + "name": "spectrum", + "parameters": [ + { + "name": "dpt", + "range": {} + } + ] + }, + { + "_id": {"$oid":"300000000000000000000002"}, + "name": "kf", + "parameters": [ + { + "name": "weight %", + "range": { + "min": 0, + "max": 1.5 + } + }, + { + "name": "standard deviation", + "range": { + "min": 0, + "max": 0.5 + } + } + ] + } ] } } \ No newline at end of file diff --git a/static/styles/swagger.css b/static/styles/swagger.css index ac69d38..33bebe1 100644 --- a/static/styles/swagger.css +++ b/static/styles/swagger.css @@ -144,6 +144,13 @@ body:after { border-color: var(--red); } +/*download button*/ +.swagger-ui .download-contents { + border-radius: 0; + height: 28px; + width: 80px; +} + /*model*/ .swagger-ui .model-box { border-radius: 0; @@ -153,6 +160,22 @@ body:after { .swagger-ui .btn.execute { background-color: var(--dark-blue); border-color: var(--dark-blue); + height: 30px; + line-height: 0.7; +} + +.swagger-ui .btn-group .btn:last-child { + border-radius: 0; + height: 30px; + border-color: var(--dark-blue); +} + +.swagger-ui .btn-group .btn:first-child { + border-radius: 0; +} + +.swagger-ui .btn-group { + padding: 0 20px; } /*parameter input*/ @@ -160,6 +183,14 @@ body:after { border-radius: 0; } +/*required label*/ +.swagger-ui .parameter__name.required > span { + color: var(--red) !important; +} + +.swagger-ui .parameter__name.required::after { + color: var(--red); +} /*Remove colored parameters bar*/ .swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after { background: none; From 20f57acd2aa031a3fbce7b4f61f6a64749d98606 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Wed, 6 May 2020 14:39:04 +0200 Subject: [PATCH 16/16] implemented first /sample methods --- api/sample.yaml | 41 +++- api/schemas.yaml | 25 ++- src/db.ts | 6 +- src/helpers/authorize.ts | 15 +- src/helpers/test.ts | 1 + src/index.ts | 3 +- src/models/note.ts | 12 ++ src/models/note_field.ts | 8 + src/models/sample.ts | 18 ++ src/routes/material.spec.ts | 11 +- src/routes/material.ts | 19 +- src/routes/sample.spec.ts | 336 ++++++++++++++++++++++++++++++ src/routes/sample.ts | 109 ++++++++++ src/routes/template.spec.ts | 1 - src/routes/template.ts | 7 +- src/routes/user.ts | 29 ++- src/routes/validate/id.ts | 11 +- src/routes/validate/material.ts | 2 +- src/routes/validate/note_field.ts | 18 ++ src/routes/validate/sample.ts | 77 +++++++ src/routes/validate/template.ts | 2 +- src/routes/validate/user.ts | 2 +- src/test/db.json | 178 +++++++++++++--- tsconfig.json | 2 + 24 files changed, 844 insertions(+), 89 deletions(-) create mode 100644 src/models/note.ts create mode 100644 src/models/note_field.ts create mode 100644 src/models/sample.ts create mode 100644 src/routes/sample.spec.ts create mode 100644 src/routes/sample.ts create mode 100644 src/routes/validate/note_field.ts create mode 100644 src/routes/validate/sample.ts diff --git a/api/sample.yaml b/api/sample.yaml index e127d74..32bb6ed 100644 --- a/api/sample.yaml +++ b/api/sample.yaml @@ -1,6 +1,6 @@ /samples: get: - summary: TODO all samples in overview + summary: all samples in overview description: 'Auth: all, levels: read, write, maintain, dev, admin' tags: - /sample @@ -10,7 +10,9 @@ content: application/json: schema: - $ref: 'api.yaml#/components/schemas/Samples' + type: array + items: + $ref: 'api.yaml#/components/schemas/SampleRefs' 401: $ref: 'api.yaml#/components/responses/401' 500: @@ -39,7 +41,7 @@ 500: $ref: 'api.yaml#/components/responses/500' put: - summary: TODO add/change sample + summary: TODO change sample description: 'Auth: basic, levels: write, maintain, dev, admin' tags: - /sample @@ -88,10 +90,41 @@ $ref: 'api.yaml#/components/responses/404' 500: $ref: 'api.yaml#/components/responses/500' + +/sample/new: + post: + summary: add sample + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /sample + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Sample' + responses: + 200: + description: samples details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/SampleRefs' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 500: + $ref: 'api.yaml#/components/responses/500' + /sample/notes/fields: get: summary: TODO list all existing field names for custom notes fields - description: 'Auth: all, levels: write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, maintain, dev, admin' tags: - /sample responses: diff --git a/api/schemas.yaml b/api/schemas.yaml index 62b4690..a7aa0e2 100644 --- a/api/schemas.yaml +++ b/api/schemas.yaml @@ -14,16 +14,17 @@ Color: example: black SampleProperties: properties: - sample_number: + number: type: string + example: Rng172 type: type: string + example: granulate batch: type: string - validated: - type: boolean + example: 1560237365 -Samples: +SampleRefs: allOf: - $ref: 'api.yaml#/components/schemas/_Id' - $ref: 'api.yaml#/components/schemas/Color' @@ -41,17 +42,23 @@ Sample: - $ref: 'api.yaml#/components/schemas/Color' - $ref: 'api.yaml#/components/schemas/SampleProperties' properties: - material: - $ref: 'api.yaml#/components/schemas/Material' + material_id: + allOf: + - $ref: 'api.yaml#/components/schemas/Id' notes: type: object properties: - comments: + comment: type: string sample_references: type: array items: - $ref: 'api.yaml#/components/schemas/Id' + properties: + id: + $ref: 'api.yaml#/components/schemas/Id' + relation: + type: string + example: part to this sample SampleDetail: allOf: - $ref: 'api.yaml#/components/schemas/_Id' @@ -63,7 +70,7 @@ SampleDetail: notes: type: object properties: - comments: + comment: type: string sample_references: type: array diff --git a/src/db.ts b/src/db.ts index 090e275..f188468 100644 --- a/src/db.ts +++ b/src/db.ts @@ -37,7 +37,7 @@ export default class db { } // connect to db - mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true}, err => { + mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true, connectTimeoutMS: 10000}, err => { if (err) done(err); }); mongoose.connection.on('error', console.error.bind(console, 'connection error:')); @@ -92,7 +92,9 @@ export default class db { Object.keys(json.collections).forEach(collectionName => { // create each collection for(let i in json.collections[collectionName]) { // convert $oid fields to actual ObjectIds Object.keys(json.collections[collectionName][i]).forEach(key => { - json.collections[collectionName][i][key] = json.collections[collectionName][i][key].hasOwnProperty('$oid') ? mongoose.Types.ObjectId(json.collections[collectionName][i][key].$oid) : json.collections[collectionName][i][key]; + if (json.collections[collectionName][i][key] !== null && json.collections[collectionName][i][key].hasOwnProperty('$oid')) { + json.collections[collectionName][i][key] = mongoose.Types.ObjectId(json.collections[collectionName][i][key].$oid); + } }) } this.state.db.createCollection(collectionName, (err, collection) => { diff --git a/src/helpers/authorize.ts b/src/helpers/authorize.ts index d3c7e75..e2f626a 100644 --- a/src/helpers/authorize.ts +++ b/src/helpers/authorize.ts @@ -9,7 +9,7 @@ import UserModel from '../models/user'; module.exports = async (req, res, next) => { let givenMethod = ''; // authorization method given by client, basic taken preferred - let user = {name: '', level: ''}; // user object + let user = {name: '', level: '', id: ''}; // user object // test authentications const userBasic = await basic(req, next); @@ -45,7 +45,8 @@ module.exports = async (req, res, next) => { req.authDetails = { method: givenMethod, username: user.name, - level: user.level + level: user.level, + id: user.id }; next(); @@ -57,12 +58,12 @@ function basic (req, next): any { // checks basic auth and returns changed user const auth = basicAuth(req); if (auth !== undefined) { // basic auth available UserModel.find({name: auth.name}).lean().exec( (err, data: any) => { // find user - if (err) next(err); + if (err) return next(err); if (data.length === 1) { // one user found bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password - if (err) next(err); + if (err) return next(err); if (res === true) { - resolve({level: data[0].level, name: data[0].name}); + resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString()}); } else { resolve(null); @@ -84,9 +85,9 @@ function key (req, next): any { // checks API key and returns changed user obje return new Promise(resolve => { if (req.query.key !== undefined) { UserModel.find({key: req.query.key}).lean().exec( (err, data: any) => { // find user - if (err) next(err); + if (err) return next(err); if (data.length === 1) { // one user found - resolve({level: data[0].level, name: data[0].name}); + resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString()}); } else { resolve(null); diff --git a/src/helpers/test.ts b/src/helpers/test.ts index afd49dd..6c2fa72 100644 --- a/src/helpers/test.ts +++ b/src/helpers/test.ts @@ -14,6 +14,7 @@ export default class TestHelper { 401: {status: 'Unauthorized'}, 403: {status: 'Forbidden'}, 404: {status: 'Not found'}, + 500: {status: 'Internal server error'} } static before (done) { diff --git a/src/index.ts b/src/index.ts index 15bd504..63ca19e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,9 +45,10 @@ app.use(require('./helpers/authorize')); // handle authentication // require routes app.use('/', require('./routes/root')); -app.use('/', require('./routes/user')); +app.use('/', require('./routes/sample')); app.use('/', require('./routes/material')); app.use('/', require('./routes/template')); +app.use('/', require('./routes/user')); // static files app.use('/static', express.static('static')); diff --git a/src/models/note.ts b/src/models/note.ts new file mode 100644 index 0000000..a13fd6a --- /dev/null +++ b/src/models/note.ts @@ -0,0 +1,12 @@ +import mongoose from 'mongoose'; + +const NoteSchema = new mongoose.Schema({ + comment: String, + sample_references: [{ + id: mongoose.Schema.Types.ObjectId, + relation: String + }], + custom_fields: mongoose.Schema.Types.Mixed +}); + +export default mongoose.model('note', NoteSchema); \ No newline at end of file diff --git a/src/models/note_field.ts b/src/models/note_field.ts new file mode 100644 index 0000000..86158e3 --- /dev/null +++ b/src/models/note_field.ts @@ -0,0 +1,8 @@ +import mongoose from 'mongoose'; + +const NoteFieldSchema = new mongoose.Schema({ + name: {type: String, index: {unique: true}}, + qty: Number +}); + +export default mongoose.model('note_field', NoteFieldSchema); \ No newline at end of file diff --git a/src/models/sample.ts b/src/models/sample.ts new file mode 100644 index 0000000..81dcc28 --- /dev/null +++ b/src/models/sample.ts @@ -0,0 +1,18 @@ +import mongoose from 'mongoose'; + +import MaterialModel from './material'; +import NoteModel from './note'; +import UserModel from './user'; + +const SampleSchema = new mongoose.Schema({ + number: {type: String, index: {unique: true}}, + type: String, + color: String, + batch: String, + validated: Boolean, + material_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialModel}, + note_id: {type: mongoose.Schema.Types.ObjectId, ref: NoteModel}, + user_id: {type: mongoose.Schema.Types.ObjectId, ref: UserModel} +}); + +export default mongoose.model('sample', SampleSchema); \ No newline at end of file diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts index c69538a..7b84c08 100644 --- a/src/routes/material.spec.ts +++ b/src/routes/material.spec.ts @@ -19,7 +19,7 @@ describe('/material', () => { }).end((err, res) => { if (err) return done(err); const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.users.length); + should(res.body).have.lengthOf(json.collections.materials.length); should(res.body).matchEach(material => { should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); should(material).have.property('_id').be.type('string'); @@ -47,7 +47,7 @@ describe('/material', () => { }).end((err, res) => { if (err) return done(err); const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.users.length); + should(res.body).have.lengthOf(json.collections.materials.length); should(res.body).matchEach(material => { should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); should(material).have.property('_id').be.type('string'); @@ -82,7 +82,7 @@ describe('/material', () => { url: '/material/100000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 200, - res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]} + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}, {color: 'natural', number: 5514263422}]} }); }); it('returns the right material for an API key', done => { @@ -127,7 +127,7 @@ describe('/material', () => { auth: {basic: 'janedoe'}, httpStatus: 200, req: {}, - res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]} + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}, {color: 'natural', number: 5514263422}]} }); }); it('keeps unchanged properties', done => { @@ -296,7 +296,6 @@ describe('/material', () => { req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]} }).end((err, res) => { if (err) return done (err); - console.log(res.body); should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); should(res.body).have.property('_id').be.type('string'); should(res.body).have.property('name', 'Crastin CE 2510'); @@ -324,7 +323,7 @@ describe('/material', () => { if (err) return done (err); MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, data: any) => { if (err) return done (err); - console.log(data[0]); + should(data).have.lengthOf(1); should(data[0]).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', '__v'); should(data[0]).have.property('_id'); should(data[0]).have.property('name', 'Crastin CE 2510'); diff --git a/src/routes/material.ts b/src/routes/material.ts index c44afa7..5628fa6 100644 --- a/src/routes/material.ts +++ b/src/routes/material.ts @@ -11,7 +11,7 @@ router.get('/materials', (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; MaterialModel.find({}).lean().exec((err, data) => { - if(err) next(err); + if (err) return next(err); res.json(data.map(e => MaterialValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors }); }); @@ -20,8 +20,7 @@ router.get('/material/' + IdValidate.parameter(), (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; MaterialModel.findById(req.params.id).lean().exec((err, data) => { - if(err) next(err); - console.log(data); + if (err) return next(err); if (data) { res.json(MaterialValidate.output(data)); } @@ -35,14 +34,14 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => { if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; const {error, value: material} = MaterialValidate.input(req.body, 'change'); - if(error !== undefined) { + if (error) { res.status(400).json({status: 'Invalid body format'}); return; } if (material.hasOwnProperty('name')) { MaterialModel.find({name: material.name}).lean().exec((err, data) => { - if(err) next(err); + if (err) return next(err); if (data.length > 0 && data[0]._id != req.params.id) { res.status(400).json({status: 'Material name already taken'}); return; @@ -58,7 +57,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => { function f() { // to resolve async MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).lean().exec((err, data) => { - if (err) next(err); + if (err) return next(err); if (data) { res.json(MaterialValidate.output(data)); } @@ -73,7 +72,7 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => { if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; MaterialModel.findByIdAndDelete(req.params.id).lean().exec((err, data) => { - if (err) next(err); + if (err) return next(err); if (data) { res.json({status: 'OK'}) } @@ -88,20 +87,20 @@ router.post('/material/new', (req, res, next) => { // validate input const {error, value: material} = MaterialValidate.input(req.body, 'new'); - if(error !== undefined) { + if (error) { res.status(400).json({status: 'Invalid body format'}); return; } MaterialModel.find({name: material.name}).lean().exec((err, data) => { - if(err) next(err); + if (err) return next(err); if (data.length > 0) { res.status(400).json({status: 'Material name already taken'}); return; } new MaterialModel(material).save((err, data) => { - if(err) next(err); + if (err) return next(err); res.json(MaterialValidate.output(data.toObject())); }); }); diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts new file mode 100644 index 0000000..857556c --- /dev/null +++ b/src/routes/sample.spec.ts @@ -0,0 +1,336 @@ +import should from 'should/as-function'; +import SampleModel from '../models/sample'; +import NoteModel from '../models/note'; +import NoteFieldModel from '../models/note_field'; +import TestHelper from "../helpers/test"; + + +describe('/sample', () => { + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + + describe('GET /samples', () => { + it('returns all samples', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id'); + should(material).have.property('_id').be.type('string'); + should(material).have.property('number').be.type('string'); + should(material).have.property('type').be.type('string'); + should(material).have.property('color').be.type('string'); + should(material).have.property('batch').be.type('string'); + should(material).have.property('material_id').be.type('string'); + should(material).have.property('note_id'); + should(material).have.property('user_id').be.type('string'); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples', + auth: {key: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id'); + should(material).have.property('_id').be.type('string'); + should(material).have.property('number').be.type('string'); + should(material).have.property('type').be.type('string'); + should(material).have.property('color').be.type('string'); + should(material).have.property('batch').be.type('string'); + should(material).have.property('material_id').be.type('string'); + should(material).have.property('note_id'); + should(material).have.property('user_id').be.type('string'); + }); + done(); + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples', + httpStatus: 401 + }); + }); + }); + + describe('POST /sample/new', () => { + it('returns the right sample', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('number', 'Rng172'); + should(res.body).have.property('color', 'black'); + should(res.body).have.property('type', 'granulate'); + should(res.body).have.property('batch', '1560237365'); + should(res.body).have.property('material_id', '100000000000000000000001'); + should(res.body).have.property('note_id').be.type('string'); + should(res.body).have.property('user_id', '000000000000000000000002'); + done(); + }); + }); + it('stores the sample', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + }).end(err => { + if (err) return done (err); + SampleModel.find({number: 'Rng172'}).lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('number', 'Rng172'); + should(data[0]).have.property('color', 'black'); + should(data[0]).have.property('type', 'granulate'); + should(data[0]).have.property('batch', '1560237365'); + should(data[0].material_id.toString()).be.eql('100000000000000000000001'); + should(data[0].user_id.toString()).be.eql('000000000000000000000002'); + should(data[0]).have.property('note_id'); + NoteModel.findById(data[0].note_id).lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.property('_id'); + should(data).have.property('comment', 'Testcomment'); + should(data).have.property('sample_references'); + should(data.sample_references).have.lengthOf(1); + should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003'); + should(data.sample_references[0]).have.property('relation', 'part to this sample'); + done(); + }); + }) + }); + }); + it('stores the custom fields', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [], custom_fields: {field1: 'a', field2: 'b', 'not allowed for new applications': true}}} + }).end((err, res) => { + if (err) return done (err); + NoteModel.findById(res.body.note_id).lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('_id'); + should(data).have.property('comment', 'Testcomment'); + should(data).have.property('sample_references').have.lengthOf(0); + should(data).have.property('custom_fields'); + should(data.custom_fields).have.property('field1', 'a'); + should(data.custom_fields).have.property('field2', 'b'); + should(data.custom_fields).have.property('not allowed for new applications', true); + NoteFieldModel.find({name: 'field1'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('qty', 1); + NoteFieldModel.find({name: 'field2'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('qty', 1); + NoteFieldModel.find({name: 'not allowed for new applications'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('qty', 3); + done(); + }); + }); + }); + }); + }); + }); + it('rejects a color not defined for the material', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'green', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Color not available for material'} + }); + }); + it('rejects an unknown material id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '000000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Material not available'} + }); + }); + it('rejects a sample number in use', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: '1', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Sample number already taken'} + }); + }); + it('rejects an invalid sample reference', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '000000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Sample reference not available'} + }); + }); + it('rejects a missing color', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects a missing sample number', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects a missing type', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects a missing batch', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects a missing material id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid material id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'user'}, + httpStatus: 403, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + httpStatus: 401, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + }); + }); + }); + + describe('GET /sample/notes/fields', () => { + it('returns all fields', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/notes/fields', + auth: {basic: 'user'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.note_fields.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('name', 'qty'); + should(material).have.property('qty').be.type('number'); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/notes/fields', + auth: {key: 'user'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.note_fields.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('name', 'qty'); + should(material).have.property('qty').be.type('number'); + }); + done(); + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/notes/fields', + httpStatus: 401 + }); + }); + }); +}); diff --git a/src/routes/sample.ts b/src/routes/sample.ts new file mode 100644 index 0000000..bbebaba --- /dev/null +++ b/src/routes/sample.ts @@ -0,0 +1,109 @@ +import express from 'express'; + +import SampleValidate from './validate/sample'; +import NoteFieldValidate from './validate/note_field'; +import SampleModel from '../models/sample' +import MaterialModel from '../models/material'; +import NoteModel from '../models/note'; +import NoteFieldModel from '../models/note_field'; + + + +const router = express.Router(); + +router.get('/samples', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + + SampleModel.find({}).lean().exec((err, data) => { + if (err) return next(err); + res.json(data.map(e => SampleValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }) +}); + + +router.post('/sample/new', (req, res, next) => { + if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + + const {error, value: sample} = SampleValidate.input(req.body, 'new'); + if (error) { + return res.status(400).json({status: 'Invalid body format'}); + } + + MaterialModel.findById(sample.material_id).lean().exec((err, data: any) => { // validate material_id + if (err) return next(err); + if (!data) { // could not find material_id + return res.status(400).json({status: 'Material not available'}); + } + if (!data.numbers.find(e => e.color === sample.color)) { // color for material not specified + return res.status(400).json({status: 'Color not available for material'}); + } + SampleModel.findOne({number: sample.number}).lean().exec((err, data) => { // validate sample number + if (err) return next(err); + if (data) { // found entry with sample number + return res.status(400).json({status: 'Sample number already taken'}); + } + + if (sample.notes.sample_references.length > 0) { // validate sample_references + let referencesCount = sample.notes.sample_references.length; + sample.notes.sample_references.forEach(reference => { + SampleModel.findById(reference.id).lean().exec((err, data) => { + if (err) return next(err); + if (!data) { + return res.status(400).json({status: 'Sample reference not available'}); + } + referencesCount --; + if (referencesCount <= 0) { + f(); + } + }); + }); + } + else { + f(); + } + + if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { + customFieldsAdd(Object.keys(sample.notes.custom_fields)); + } + + function f() { // to resolve async + new NoteModel(sample.notes).save((err, data) => { + if (err) return next(err); + delete sample.notes; + sample.note_id = data._id; + sample.user_id = req.authDetails.id; + new SampleModel(sample).save((err, data) => { + if (err) return next(err); + res.json(SampleValidate.output(data.toObject())); + }); + }); + } + }); + }) +}); + +router.get('/sample/notes/fields', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + + NoteFieldModel.find({}).lean().exec((err, data) => { + if (err) return next(err); + res.json(data.map(e => NoteFieldValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }) +}); + + +module.exports = router; + + +function customFieldsAdd (fields) { + fields.forEach(field => { + NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: 1}}).lean().exec((err, data) => { // check if field exists + if (err) return console.error(err); + if (!data) { // new field + new NoteFieldModel({name: field, qty: 1}).save(err => { + if (err) return console.error(err); + }) + } + }); + }); +} \ No newline at end of file diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts index 5ee4d1a..68b3d4a 100644 --- a/src/routes/template.spec.ts +++ b/src/routes/template.spec.ts @@ -172,7 +172,6 @@ describe('/template', () => { if (err) return done(err); TemplateTreatmentModel.find({name: 'heat aging'}).lean().exec((err, data:any) => { if (err) return done(err); - console.log(data); should(data).have.lengthOf(1); should(data[0]).have.only.keys('_id', 'name', 'parameters', '__v'); should(data[0]).have.property('name', 'heat aging'); diff --git a/src/routes/template.ts b/src/routes/template.ts index 7e4aee7..1e859cd 100644 --- a/src/routes/template.ts +++ b/src/routes/template.ts @@ -41,7 +41,7 @@ router.put('/template/:collection(measurement|treatment)/:name', (req, res, next if (err) next (err); const templateState = data? 'change': 'new'; const {error, value: template} = TemplateValidate.input(req.body, templateState); - if(error !== undefined) { + if (error) { res.status(400).json({status: 'Invalid body format'}); return; } @@ -64,7 +64,7 @@ router.put('/template/:collection(measurement|treatment)/:name', (req, res, next function f() { // to resolve async collectionModel.findOneAndUpdate({name: req.params.name}, template, {new: true, upsert: true}).lean().exec((err, data) => { - if (err) next(err); + if (err) return next(err); res.json(TemplateValidate.output(data)); }); } @@ -76,7 +76,7 @@ router.delete('/template/:collection(measurement|treatment)/:name', (req, res, n (req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel) .findOneAndDelete({name: req.params.name}).lean().exec((err, data) => { - if (err) next(err); + if (err) return next(err); if (data) { res.json({status: 'OK'}) } @@ -87,5 +87,4 @@ router.delete('/template/:collection(measurement|treatment)/:name', (req, res, n }); - module.exports = router; \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts index c60dd7b..a0161f9 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -27,7 +27,7 @@ router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi } UserModel.findOne({name: username}).lean().exec( (err, data:any) => { - if (err) next(err); + if (err) return next(err); if (data) { res.json(UserValidate.output(data)); // validate all and filter null values from validation errors } @@ -46,7 +46,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi username = req.params.username; } const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : '')); - if(error !== undefined) { + if (error) { res.status(400).json({status: 'Invalid body format'}); return; } @@ -58,14 +58,14 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi // check that user does not already exist if new name was specified if (user.hasOwnProperty('name') && user.name !== username) { UserModel.find({name: user.name}).lean().exec( (err, data:any) => { - if (err) next(err); + if (err) return next(err); if (data.length > 0 || UserValidate.isSpecialName(user.name)) { res.status(400).json({status: 'Username already taken'}); return; } UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => { - if (err) next(err); + if (err) return next(err); if (data) { res.json(UserValidate.output(data)); } @@ -77,7 +77,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi } else { UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => { - if (err) next(err); + if (err) return next(err); if (data) { res.json(UserValidate.output(data)); // validate all and filter null values from validation errors } @@ -98,7 +98,7 @@ router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // } UserModel.findOneAndDelete({name: username}).lean().exec( (err, data:any) => { - if (err) next(err); + if (err) return next(err); if (data) { res.json({status: 'OK'}) } @@ -109,11 +109,10 @@ router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // }); router.get('/user/key', (req, res, next) => { - console.log('hmm'); if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; UserModel.findOne({name: req.authDetails.username}).lean().exec( (err, data:any) => { - if (err) next(err); + if (err) return next(err); res.json({key: data.key}); }); }); @@ -123,14 +122,14 @@ router.post('/user/new', (req, res, next) => { // validate input const {error, value: user} = UserValidate.input(req.body, 'new'); - if(error !== undefined) { + if (error) { res.status(400).json({status: 'Invalid body format'}); return; } // check that user does not already exist UserModel.find({name: user.name}).lean().exec( (err, data:any) => { - if (err) next(err); + if (err) return next(err); if (data.length > 0 || UserValidate.isSpecialName(user.name)) { res.status(400).json({status: 'Username already taken'}); return; @@ -140,7 +139,7 @@ router.post('/user/new', (req, res, next) => { bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing user.pass = hash; new UserModel(user).save((err, data) => { // store user - if (err) next(err); + if (err) return next(err); res.json(UserValidate.output(data.toObject())); }); }); @@ -150,15 +149,15 @@ router.post('/user/new', (req, res, next) => { router.post('/user/passreset', (req, res, next) => { // check if user/email combo exists UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( (err, data: any) => { - if (err) next(err); + if (err) return next(err); if (data.length === 1) { // it exists const newPass = Math.random().toString(36).substring(2); bcrypt.hash(newPass, 10, (err, hash) => { // password hashing - if (err) next(err); + if (err) return next(err); UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}, err => { // write new password - if (err) next(err); + if (err) return next(err); mail(data[0].email, 'Your new password for the DFOP database', 'Hi,

    You requested to reset your password.
    Your new password is:

    ' + newPass + '

    If you did not request a password reset, talk to the sysadmin quickly!

    Have a nice day.

    The DFOP team', err => { - if (err) next(err); + if (err) return next(err); res.json({status: 'OK'}); }); }); diff --git a/src/routes/validate/id.ts b/src/routes/validate/id.ts index 84024e9..5409993 100644 --- a/src/routes/validate/id.ts +++ b/src/routes/validate/id.ts @@ -11,7 +11,16 @@ export default class IdValidate { return this.id.validate(id).error === undefined; } - static parameter() { // :id url parameter + static parameter () { // :id url parameter return ':id([0-9a-f]{24})'; } + + static stringify (data) { + Object.keys(data).forEach(key => { + if (data[key] !== null && data[key].hasOwnProperty('_bsontype') && data[key]._bsontype === 'ObjectID') { + data[key] = data[key].toString(); + } + }); + return data; + } } \ No newline at end of file diff --git a/src/routes/validate/material.ts b/src/routes/validate/material.ts index c5ac005..54cd749 100644 --- a/src/routes/validate/material.ts +++ b/src/routes/validate/material.ts @@ -66,7 +66,7 @@ export default class MaterialValidate { // validate input for material } static output (data) { // validate output from database for needed properties, strip everything else - data._id = data._id.toString(); + data = IdValidate.stringify(data); const {value, error} = joi.object({ _id: IdValidate.get(), name: this.material.name, diff --git a/src/routes/validate/note_field.ts b/src/routes/validate/note_field.ts new file mode 100644 index 0000000..4892f22 --- /dev/null +++ b/src/routes/validate/note_field.ts @@ -0,0 +1,18 @@ +import joi from '@hapi/joi'; + +export default class NoteFieldValidate { + private static note_field = { + name: joi.string() + .max(128), + + qty: joi.number() + }; + + static output (data) { + const {value, error} = joi.object({ + name: this.note_field.name, + qty: this.note_field.qty + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} \ No newline at end of file diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts new file mode 100644 index 0000000..d94cede --- /dev/null +++ b/src/routes/validate/sample.ts @@ -0,0 +1,77 @@ +import joi from '@hapi/joi'; + +import IdValidate from './id'; + +export default class SampleValidate { + private static sample = { + number: joi.string() + .max(128), + + color: joi.string() + .max(128), + + type: joi.string() + .max(128), + + batch: joi.string() + .max(128) + .allow(''), + + notes: joi.object({ + comment: joi.string() + .max(512), + + sample_references: joi.array() + .items(joi.object({ + id: IdValidate.get(), + + relation: joi.string() + .max(128) + })), + + custom_fields: joi.object() + .pattern(/.*/, joi.alternatives() + .try( + joi.string().max(128), + joi.number(), + joi.boolean(), + joi.date() + ) + ) + }) + }; + + static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated) + if (param === 'new') { + return joi.object({ + number: this.sample.number.required(), + color: this.sample.color.required(), + type: this.sample.type.required(), + batch: this.sample.batch.required(), + material_id: IdValidate.get().required(), + notes: this.sample.notes.required() + }).validate(data); + } + else if (param === 'change') { + return{error: 'Not implemented!', value: {}}; + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } + + static output (data) { + data = IdValidate.stringify(data); + const {value, error} = joi.object({ + _id: IdValidate.get(), + number: this.sample.number, + color: this.sample.color, + type: this.sample.type, + batch: this.sample.batch, + material_id: IdValidate.get(), + note_id: IdValidate.get().allow(null), + user_id: IdValidate.get() + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} \ No newline at end of file diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts index 6a0a23f..a279dce 100644 --- a/src/routes/validate/template.ts +++ b/src/routes/validate/template.ts @@ -48,7 +48,7 @@ export default class TemplateValidate { } static output (data) { // validate output from database for needed properties, strip everything else - data._id = data._id.toString(); + data = IdValidate.stringify(data); const {value, error} = joi.object({ _id: IdValidate.get(), name: this.template.name, diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index 4b1259a..150bf64 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -69,7 +69,7 @@ export default class UserValidate { // validate input for user } static output (data) { // validate output from database for needed properties, strip everything else - data._id = data._id.toString(); + data = IdValidate.stringify(data); const {value, error} = joi.object({ _id: IdValidate.get(), name: this.user.name, diff --git a/src/test/db.json b/src/test/db.json index d1bca35..2d8a7d0 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -1,38 +1,93 @@ { "collections": { - "users": [ + "samples": [ { - "_id": {"$oid":"000000000000000000000001"}, - "email": "user@bosch.com", - "name": "user", - "pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi", - "level": "read", - "location": "Rng", - "device_name": "Alpha I", - "key": "000000000000000000001001", + "_id": {"$oid":"400000000000000000000001"}, + "number": "1", + "type": "granulate", + "color": "black", + "batch": "", + "validated": true, + "material_id": {"$oid":"100000000000000000000004"}, + "note_id": null, + "user_id": {"$oid":"000000000000000000000002"}, "__v": 0 }, { - "_id": {"$oid":"000000000000000000000002"}, - "email": "jane.doe@bosch.com", - "name": "janedoe", - "pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi", - "level": "write", - "location": "Rng", - "device_name": "Alpha I", - "key": "000000000000000000001002", + "_id": {"$oid":"400000000000000000000002"}, + "number": "21", + "type": "granulate", + "color": "natural", + "batch": "1560237365", + "validated": true, + "material_id": {"$oid":"100000000000000000000001"}, + "note_id": {"$oid":"500000000000000000000001"}, + "user_id": {"$oid":"000000000000000000000002"}, "__v": 0 }, { - "_id": {"$oid":"000000000000000000000003"}, - "email": "a.d.m.i.n@bosch.com", - "name": "admin", - "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K", - "level": "admin", - "location": "Rng", - "device_name": "", - "key": "000000000000000000001003", - "__v": "0" + "_id": {"$oid":"400000000000000000000003"}, + "number": "33", + "type": "part", + "color": "black", + "batch": "1704-005", + "validated": false, + "material_id": {"$oid":"100000000000000000000005"}, + "note_id": {"$oid":"500000000000000000000002"}, + "user_id": {"$oid":"000000000000000000000003"}, + "__v": 0 + }, + { + "_id": {"$oid":"400000000000000000000004"}, + "number": "32", + "type": "granulate", + "color": "black", + "batch": "1653000308", + "validated": false, + "material_id": {"$oid":"100000000000000000000005"}, + "note_id": {"$oid":"500000000000000000000003"}, + "user_id": {"$oid":"000000000000000000000003"}, + "__v": 0 + } + ], + "notes": [ + { + "_id": {"$oid":"500000000000000000000001"}, + "comment": "Stoff gesperrt", + "sample_references": [], + "__v": 0 + }, + { + "_id": {"$oid":"500000000000000000000002"}, + "comment": "", + "sample_references": [{ + "id": "400000000000000000000004", + "relation": "granulate to sample" + }], + "custom_fields": { + "not allowed for new applications": true + }, + "__v": 0 + }, + { + "_id": {"$oid":"500000000000000000000003"}, + "comment": "", + "sample_references": [{ + "id": "400000000000000000000003", + "relation": "part to sample" + }], + "custom_fields": { + "not allowed for new applications": true + }, + "__v": 0 + } + ], + "note_fields": [ + { + "_id": {"$oid":"600000000000000000000001"}, + "name": "not allowed for new applications", + "qty": 2, + "__v": 0 } ], "materials": [ @@ -48,6 +103,10 @@ { "color": "black", "number": 5514263423 + }, + { + "color": "natural", + "number": 5514263422 } ], "__v": 0 @@ -83,6 +142,38 @@ "numbers": [ ], "__v": 0 + }, + { + "_id": {"$oid":"100000000000000000000004"}, + "name": "Schulamid 66 GF 25 H", + "supplier": "Schulmann", + "group": "PA66", + "mineral": 0, + "glass_fiber": 25, + "carbon_fiber": 0, + "numbers": [ + { + "color": "black", + "number": 5513933405 + } + ], + "__v": 0 + }, + { + "_id": {"$oid":"100000000000000000000005"}, + "name": "Amodel A 1133 HS", + "supplier": "Solvay", + "group": "PPA", + "mineral": 0, + "glass_fiber": 33, + "carbon_fiber": 0, + "numbers": [ + { + "color": "black", + "number": 5514262406 + } + ], + "__v": 0 } ], "treatment_templates": [ @@ -150,6 +241,41 @@ } ] } + ], + "users": [ + { + "_id": {"$oid":"000000000000000000000001"}, + "email": "user@bosch.com", + "name": "user", + "pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi", + "level": "read", + "location": "Rng", + "device_name": "Alpha I", + "key": "000000000000000000001001", + "__v": 0 + }, + { + "_id": {"$oid":"000000000000000000000002"}, + "email": "jane.doe@bosch.com", + "name": "janedoe", + "pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi", + "level": "write", + "location": "Rng", + "device_name": "Alpha I", + "key": "000000000000000000001002", + "__v": 0 + }, + { + "_id": {"$oid":"000000000000000000000003"}, + "email": "a.d.m.i.n@bosch.com", + "name": "admin", + "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K", + "level": "admin", + "location": "Rng", + "device_name": "", + "key": "000000000000000000001003", + "__v": "0" + } ] } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8bbe445..b43a5fb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,8 @@ "sourceMap": true, "esModuleInterop": true, "resolveJsonModule": true, + "incremental": true, + "diagnostics": true, "typeRoots": [ "src/customTypings", "node_modules/@types"