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"