diff --git a/.idea/dictionaries/VLE2FE.xml b/.idea/dictionaries/VLE2FE.xml index 5337928..a241715 100644 --- a/.idea/dictionaries/VLE2FE.xml +++ b/.idea/dictionaries/VLE2FE.xml @@ -1,12 +1,48 @@ + akro + amodel + anwendungsbeschränkt + batchgranulate bcrypt + bnpd cfenv + colordesignatiomsuppl + colordesignationsuppl + contentin + definma dfopdb + dosiergeschw + dpts + einspritzgeschw + frameguard + functionlink + glassfibrecontent + isin janedoe + johnnydoe + kfingew + latamid + lati + lyucy + materialnumber pagesize + pnach + preaged + reinforcementmaterial + reinforcingmaterial + samplenumber + sdpt + signalviolet + solvay + spaceless + stabwn + stanyl + stringin testcomment + ultramid + vorgealtert \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 7e46df7..285ddb5 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,6 +2,7 @@ \ No newline at end of file diff --git a/api/api.yaml b/api/api.yaml index a1966fa..45be8c4 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -5,28 +5,35 @@ info: title: Digital fingerprint of plastics - API version: 1.0.0 description: | - This API gives access to the project database.
+ 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
+ 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:
- + if the entered authentication is allowed. + + + There are a number of different user levels: + + | | read sample data | add samples/edit own | read spectral data | edit other's data | maintain templates | edit users | + |:-----:|:----------------:|:--------------------:|:------------------:|:-----------------:|:------------------:|:----------:| + | read | yes | no | no | no | no | no | + | write | yes | yes | no | no | no | no | + | dev | yes | yes | yes | yes | yes | no | + | admin | yes | yes | yes | yes | yes | yes | + Password policy: - + + - at least one digit + - at least one lower case letter + - at least one upper case letter + - at least one of the following special characters: !"#%&'()*+,-./:;<=>?@[\]^_`{|}~ + - no whitespace + - at least 8 characters + +
+ x-doc: | status: - Bitbucket repository -# TODO: Link to new documentation page + Bitbucket repository API + Bitbucket repository UI + Documentation page servers: diff --git a/api/material.yaml b/api/material.yaml index 593afb1..548bce8 100644 --- a/api/material.yaml +++ b/api/material.yaml @@ -1,7 +1,7 @@ /materials: get: summary: lists all materials - description: 'Auth: all, levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, dev, admin' x-doc: returns only materials with status 10 tags: - /material @@ -31,7 +31,7 @@ - $ref: 'api.yaml#/components/parameters/State' get: summary: lists all new/deleted materials - description: 'Auth: basic, levels: maintain, admin' + description: 'Auth: basic, levels: dev, admin' x-doc: returns materials with status 0/-1 tags: - /material @@ -54,8 +54,8 @@ - $ref: 'api.yaml#/components/parameters/Id' get: summary: get material details - description: 'Auth: all, levels: read, write, maintain, dev, admin' - x-doc: deleted samples are available only for maintain/admin + description: 'Auth: all, levels: read, write, dev, admin' + x-doc: deleted samples are available only for dev/admin tags: - /material responses: @@ -73,7 +73,7 @@ $ref: 'api.yaml#/components/responses/500' put: summary: change material - description: 'Auth: basic, levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, dev, admin' x-doc: status is reset to 0 on any changes, deleted samples cannot be changed tags: - /material @@ -104,7 +104,7 @@ $ref: 'api.yaml#/components/responses/500' delete: summary: delete material - description: 'Auth: basic, levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, dev, admin' x-doc: sets status to -1 tags: - /material @@ -129,7 +129,7 @@ - $ref: 'api.yaml#/components/parameters/Id' put: summary: restore material - description: 'Auth: basic, levels: maintain, admin' + description: 'Auth: basic, levels: dev, admin' x-doc: status is set to 0 tags: - /material @@ -152,7 +152,7 @@ - $ref: 'api.yaml#/components/parameters/Id' put: summary: restore material - description: 'Auth: basic, levels: maintain, admin' + description: 'Auth: basic, levels: dev, admin' x-doc: status is set to 10 tags: - /material @@ -173,7 +173,7 @@ /material/new: post: summary: add material - description: 'Auth: basic, levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, dev, admin' x-doc: 'Adds status: 0 automatically' tags: - /material @@ -204,7 +204,7 @@ /material/groups: get: summary: list all existing material groups - description: 'Auth: all, levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, dev, admin' tags: - /material responses: @@ -227,7 +227,7 @@ /material/suppliers: get: summary: list all existing material suppliers - description: 'Auth: all, levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, dev, admin' tags: - /material responses: diff --git a/api/measurement.yaml b/api/measurement.yaml index 0c29e77..453b5e6 100644 --- a/api/measurement.yaml +++ b/api/measurement.yaml @@ -3,8 +3,8 @@ - $ref: 'api.yaml#/components/parameters/Id' get: summary: measurement values by id - description: 'Auth: all, levels: read, write, maintain, dev, admin' - x-doc: deleted samples are available only for maintain/admin + description: 'Auth: all, levels: read, write, dev, admin, spectral data can only be accessed by dev and admin' + x-doc: deleted samples are available only for dev/admin tags: - /measurement responses: @@ -24,7 +24,7 @@ $ref: 'api.yaml#/components/responses/500' put: summary: change measurement - description: 'Auth: basic, levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, dev, admin' x-doc: status is reset to 0 on any changes, deleted measurements cannot be edited tags: - /measurement @@ -57,7 +57,7 @@ $ref: 'api.yaml#/components/responses/500' delete: summary: delete measurement - description: 'Auth: basic, levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, dev, admin' x-doc: sets status to -1 tags: - /measurement @@ -82,7 +82,7 @@ - $ref: 'api.yaml#/components/parameters/Id' put: summary: restore measurement - description: 'Auth: basic, levels: maintain, admin' + description: 'Auth: basic, levels: dev, admin' x-doc: status is set to 0 tags: - /measurement @@ -105,7 +105,7 @@ - $ref: 'api.yaml#/components/parameters/Id' put: summary: set measurement status to validated - description: 'Auth: basic, levels: maintain, admin' + description: 'Auth: basic, levels: dev, admin' x-doc: status is set to 10 tags: - /measurement @@ -126,7 +126,7 @@ /measurement/new: post: summary: add measurement - description: 'Auth: basic, levels: write, maintain, dev, admin' + description: 'Auth: basic, levels: write, dev, admin' x-doc: 'Adds status: 0 automatically' tags: - /measurement diff --git a/api/root.yaml b/api/root.yaml index af618a7..49a1a50 100644 --- a/api/root.yaml +++ b/api/root.yaml @@ -21,7 +21,7 @@ /authorized: get: summary: Checks authorization - description: 'Auth: all, levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, dev, admin' tags: - / responses: @@ -40,6 +40,8 @@ level: type: string example: read + user_id: + $ref: 'api.yaml#/components/schemas/Id' 401: $ref: 'api.yaml#/components/responses/401' 500: @@ -67,7 +69,9 @@ example: 30 get: summary: get changelog - description: 'Auth: basic, levels: maintain, admin
Displays all logs older than timestamp, sorted by date descending, page defaults to 0, pagesize defaults to 25
Avoid using high page numbers for older logs, better use an older timestamp' + description: 'Auth: basic, levels: dev, admin
Displays all logs older than timestamp, sorted by date descending, + page defaults to 0, pagesize defaults to 25
Avoid using high page numbers for older logs, better use an older + timestamp' tags: - / responses: diff --git a/api/sample.yaml b/api/sample.yaml index 82d6c7c..b6689d9 100644 --- a/api/sample.yaml +++ b/api/sample.yaml @@ -1,8 +1,9 @@ /samples: get: summary: all samples in overview - description: 'Auth: all, levels: read, write, maintain, dev, admin' - x-doc: 'Limitations: paging and csv output does not work when including the spectrum measurement fields as well as the returned number of total samples' + description: 'Auth: all, levels: read, write, dev, admin, spectral data can only be accessed by dev and admin' + x-doc: 'Limitations: paging and csv output does not work when including the spectrum measurement fields as well as + the returned number of total samples' tags: - /sample parameters: @@ -19,7 +20,8 @@ type: string example: 5ea0450ed851c30a90e70894 - name: to-page - description: relative change of pages, use negative values to get back, defaults to 0, works only together with page-size + description: 'relative change of pages, use negative values to get back, defaults to 0, works only together with + page-size' in: query schema: type: string @@ -43,7 +45,8 @@ type: boolean example: false - name: fields[] - description: the fields to include in the output as array, defaults to ['_id','number','type','batch','material_id','color','condition','note_id','user_id','added'] + description: "the fields to include in the output as array, defaults to ['_id', 'number', 'type', + 'batch', 'material_id', 'color', 'condition', 'note_id', 'user_id', 'added']" in: query schema: type: array @@ -51,19 +54,23 @@ type: string example: ['number', 'batch'] - name: filters[] - description: "the filters to apply as an array of URIComponent encoded objects in the form {mode: 'eq/ne/lt/lte/gt/gte/in/nin', field: 'material.m', values: ['15']} using encodeURIComponent(JSON.stringify({}))" + description: "the filters to apply as an array of URIComponent encoded objects in the form {mode: + 'eq/ne/lt/lte/gt/gte/in/nin', field: 'material.m', values: ['15']} using encodeURIComponent(JSON.stringify({}))" in: query schema: type: array items: type: string - example: ["%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.m%22%2C%22values%22%3A%5B%2215%22%5D%7D", "%7B%22mode%22%3A%22isin%22%2C%22field%22%3A%22material.supplier%22%2C%22values%22%3A%5B%22BASF%22%2C%22DSM%22%5D%7D"] + example: '["%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.m%22%2C%22values%22%3A%5B%2215%22%5D%7D", + "%7B%22mode%22%3A%22isin%22%2C%22field%22%3A%22material.supplier%22%2C%22values%22%3A%5B%22BASF%22%2C%22DSM%22 + %5D%7D"]' responses: 200: description: samples overview (if the csv parameter is set, this is in CSV instead of JSON format) headers: x-total-items: - description: Total number of available items when from-id is not specified and spectrum field is not included + description: Total number of available items when from-id is not specified and spectrum field is not + included schema: type: integer example: 243 @@ -87,7 +94,7 @@ - $ref: 'api.yaml#/components/parameters/State' get: summary: all new/deleted samples in overview - description: 'Auth: basic, levels: maintain, admin' + description: 'Auth: basic, levels: admin' x-doc: returns only samples with status 0/-1 tags: - /sample @@ -108,7 +115,7 @@ /samples/count: get: summary: total number of samples - description: 'Auth: all, levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, dev, admin' tags: - /sample responses: @@ -129,8 +136,9 @@ - $ref: 'api.yaml#/components/parameters/Id' get: summary: sample details - description: 'Auth: all, levels: read, write, maintain, dev, admin
Returns validated as well as new measurements' - x-doc: deleted samples are available only for maintain/admin + description: 'Auth: all, levels: read, write, dev, admin, spectral data can only be accessed by dev and admin
+ Returns validated as well as new measurements' + x-doc: deleted samples are available only for dev/admin tags: - /sample responses: @@ -150,7 +158,8 @@ $ref: 'api.yaml#/components/responses/500' put: summary: change sample - description: 'Auth: basic, levels: write, maintain, dev, admin
Only maintain and admin are allowed to edit samples created by another user' + description: 'Auth: basic, levels: write, dev, admin
+ Only dev and admin are allowed to edit samples created by another user' x-doc: status is reset to 0 on any changes, deleted samples cannot be changed tags: - /sample @@ -181,8 +190,10 @@ $ref: 'api.yaml#/components/responses/500' delete: summary: delete sample - description: 'Auth: basic, levels: write, maintain, dev, admin
Only maintain and admin are allowed to edit samples created by another user' - x-doc: sets status to -1, notes and references to this sample are also kept, only note_fields are updated accordingly + description: 'Auth: basic, levels: write, dev, admin
+ Only dev and admin are allowed to edit samples created by another user' + x-doc: sets status to -1, notes and references to this sample are also kept, only note_fields are updated + accordingly tags: - /sample security: @@ -206,8 +217,9 @@ - $ref: 'api.yaml#/components/parameters/Number' get: summary: sample details - description: 'Auth: all, levels: read, write, maintain, dev, admin
Returns validated as well as new measurements' - x-doc: deleted samples are available only for maintain/admin + description: 'Auth: all, levels: read, write, dev, admin, spectral data can only be accessed by dev and admin
+ Returns validated as well as new measurements' + x-doc: deleted samples are available only for dev/admin tags: - /sample responses: @@ -231,7 +243,7 @@ - $ref: 'api.yaml#/components/parameters/Id' put: summary: restore sample - description: 'Auth: basic, levels: maintain, admin' + description: 'Auth: basic, levels: dev, admin' x-doc: status is set to 0 tags: - /sample @@ -254,7 +266,7 @@ - $ref: 'api.yaml#/components/parameters/Id' put: summary: set sample status to validated - description: 'Auth: basic, levels: maintain, admin' + description: 'Auth: basic, levels: dev, admin' x-doc: status is set to 10 tags: - /sample @@ -277,7 +289,8 @@ /sample/new: post: summary: add sample - description: 'Auth: basic, levels: write, maintain, dev, admin. Number property is only for admin when adding existing samples' + description: 'Auth: basic, levels: write, dev, admin. Number property is only for admin when adding existing + samples' x-doc: 'Adds status: 0 automatically' tags: - /sample @@ -313,7 +326,7 @@ /sample/notes/fields: get: summary: list all existing field names for custom notes fields - description: 'Auth: all, levels: read, write, maintain, dev, admin' + description: 'Auth: all, levels: read, write, dev, admin' x-doc: integrity has to be ensured tags: - /sample diff --git a/api/template.yaml b/api/template.yaml index 6af1294..2306749 100644 --- a/api/template.yaml +++ b/api/template.yaml @@ -3,7 +3,7 @@ - $ref: 'api.yaml#/components/parameters/Collection' get: summary: all available templates - description: 'Auth: basic, levels: read, write, maintain, dev, admin' + description: 'Auth: basic, levels: read, write, dev, admin' tags: - /template security: @@ -28,7 +28,7 @@ - $ref: 'api.yaml#/components/parameters/Id' get: summary: template details - description: 'Auth: basic, levels: read, write, maintain, admin' + description: 'Auth: basic, levels: read, write, dev, admin' tags: - /template security: @@ -48,7 +48,7 @@ $ref: 'api.yaml#/components/responses/500' put: summary: change template - description: 'Auth: basic, levels: maintain, admin' + description: 'Auth: basic, levels: dev, admin' x-doc: With a change a new version is set, resulting in a new template with a new id tags: - /template @@ -83,7 +83,7 @@ - $ref: 'api.yaml#/components/parameters/Collection' post: summary: add template - description: 'Auth: basic, levels: maintain, admin' + description: 'Auth: basic, levels: dev, admin' tags: - /template security: diff --git a/api/user.yaml b/api/user.yaml index 757ebf0..0d08408 100644 --- a/api/user.yaml +++ b/api/user.yaml @@ -24,7 +24,7 @@ /user: get: summary: list own user details - description: 'Auth: basic, levels: read, write, maintain, admin' + description: 'Auth: basic, levels: read, write, dev, admin' tags: - /user security: @@ -44,7 +44,7 @@ $ref: 'api.yaml#/components/responses/500' put: summary: change user details - description: 'Auth: basic, levels: read, write, maintain, admin' + description: 'Auth: basic, levels: read, write, dev, admin' tags: - /user security: @@ -86,7 +86,7 @@ $ref: 'api.yaml#/components/responses/500' delete: summary: delete user - description: 'Auth: basic, levels: read, write, maintain, admin' + description: 'Auth: basic, levels: read, write, dev, admin' tags: - /user security: @@ -174,7 +174,7 @@ /user/key: get: summary: get API key for the user - description: 'Auth: basic, levels: read, write, maintain, dev, admin' + description: 'Auth: basic, levels: read, write, dev, admin' tags: - /user security: diff --git a/data_import/import.js b/data_import/import.js index 0f8d867..565336a 100644 --- a/data_import/import.js +++ b/data_import/import.js @@ -47,8 +47,10 @@ let sampleDevices = {}; const sampleReferences = []; // references to other samples in format {sample, referencedSample, relation} let commentsLog = []; let customFieldsLog = []; -const vzValues = {}; // vz values from comments +const vnValues = {}; // vn values from comments const dptLog = []; +const dptSampleAddLog = []; // log samples created during dpt insertion +const typeLog = []; // TODO: conditions @@ -71,10 +73,11 @@ async function main() { await importCsv(docs[i]); await allSamples(); await saveSamples(); - await allKfVz(); + await allMcVn(); } // write logs fs.writeFileSync('./data_import/comments.txt', commentsLog.join('\r\n')); + fs.writeFileSync('./data_import/typeLog.txt', typeLog.join('\r\n')); fs.writeFileSync('./data_import/customFields.txt', customFieldsLog.join('\r\n')); fs.writeFileSync('./data_import/sampleReferences.txt', sampleReferences.map(e => JSON.stringify(e)).join('\r\n')); fs.writeFileSync('./data_import/sampleReferences.json', JSON.stringify(sampleReferences)); @@ -84,6 +87,7 @@ async function main() { if (stages.dpt) { // DPT await allDpts(); fs.writeFileSync('./data_import/sdptLog.txt', dptLog.join('\r\n')); + fs.writeFileSync('./data_import/dptSampleAddLog.txt', dptSampleAddLog.join('\r\n')); } if (0) { // pdf test console.log(await readPdf('N28_BN05-OX023_2019-07-16.pdf')); @@ -95,24 +99,23 @@ async function main() { } async function importCsv(doc) { - // Uniform name samplenumber materialnumber materialname supplier material plastic reinforcingmaterial granulate/part color charge/batch comments vz(ml/g) kfingew% degradation(%) glassfibrecontent(%) stabwn - // Metadata__AnP2.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material, granulate/Part,Color,Charge/ Batch, Comments - // Metadata__AnP2_A.csv Sample number,Material number,Material name,Supplier, Plastic,Reinforcing material, Granulate/Part, Comments, Humidity [ppm] - // Metadata__AnP2_B.csv Sample number,Material number,Material name,Supplier, Plastic,Reinforcing material, Granulate/Part, VZ [ml/g], glass fibre content - // Metadata_Ap.csv Sample number,Material number,Material name,Supplier, Plastic,Reinforcing material, Granulate/Part,Color,Charge/Batch, Comments - // Metadata_Bj.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material, Granulate/Part,Color,Charge/batch granulate/part,Comments - // Metadata_Eh.csv Sample number,Material number,Material name,Supplier,Material, Reinforcing material, Granulate/Part,Color,Charge/Batch granulate/part,Comments, VZ [cm³/g], Spalte1 - // Metadata_Eh_B.csv Sample number, Material name,Supplier, Plastic,Reinforcing material, Granulate/Part,Color, Comments, VZ [cm³/g] - // Metadata_Eh_Duroplasten.csv Sample number,Material number,Material name,Supplier,Material, Reinforcing material, Granulate/Part,Color,Charge/Batch granulate/part,Comments - // Metadata_Rng_aktuell.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material, Granulate/Part,Color,Charge/batch granulate/part,Comments, VZ (ml/g), Degradation(%),Glas fibre content (%) - // Metadata_Rng_aktuell_A.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material, Granulate/Part,Farbe,Charge/batch granulate/part,Comments, KF in Gew%, Stabwn - // Metadata_Rng_aktuell_B.csv Sample number, Material name,Supplier, Plastic,Reinforcing material (content in %),Granulate/Part, Comments, VZ (ml/g), Degradation (%), Alterungszeit in h - // Metadata_WaP.csv Probennummer, Name, Firma, Material, Teil/Rohstoff, Charge, Anmerkung,VZ (ml/g), Abbau (%), Verstärkungsstoffgehalt (%), Versuchsnummer + // Uniform name samplenumber materialnumber materialname supplier material plastic reinforcingmaterial granulate/part color charge/batch comments vz(ml/g) kfingew% degradation(%) reinforcingmaterialcontent stabwn + // Metadata__AnP2.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material,granulate/Part,Color,Charge/ Batch, Comments + // Metadata__AnP2_A.csv Sample number,Material number,Material name,Supplier, Plastic,Reinforcing material,Granulate/Part, Comments, Humidity [ppm] + // Metadata__AnP2_B.csv Sample number,Material number,Material name,Supplier, Plastic,Reinforcing material,Granulate/Part, VZ [ml/g], glass fibre content + // Metadata_Ap.csv Sample number,Material number,Material name,Supplier, Plastic,Reinforcing material,Granulate/Part,Color,Charge/Batch, Comments + // Metadata_Bj.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material,Granulate/Part,Color,Charge/batch granulate/part,Comments + // Metadata_Eh.csv Sample number,Material number,Material name,Supplier,Material, Reinforcing material,Granulate/Part,Color,Charge/Batch granulate/part,Comments, VZ [cm³/g], Spalte1 + // Metadata_Eh_B.csv Sample number, Material name,Supplier, Plastic,Reinforcing material,Granulate/Part,Color, Comments, VZ [cm³/g] + // Metadata_Eh_Duroplasten.csv Sample number,Material number,Material name,Supplier,Material, Reinforcing material,Granulate/Part,Color,Charge/Batch granulate/part,Comments + // Metadata_Rng_aktuell.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material,Granulate/Part,Color,Charge/batch granulate/part,Comments, VZ (ml/g), Degradation(%),Glas fibre content (%) + // Metadata_Rng_aktuell_A.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material,Granulate/Part,Farbe,Charge/batch granulate/part,Comments, KF in Gew%, Reinforcing material (content in %),Stabwn + // Metadata_Rng_aktuell_B.csv Sample number, Material name,Supplier, Plastic, Granulate/Part, Comments, VZ (ml/g), Degradation (%), Alterungszeit in h + // Metadata_WaP.csv Probennummer, Name, Firma, Material, Teil/Rohstoff, Charge, Anmerkung,VZ (ml/g), Abbau (%), Verstärkungsstoffgehalt (%), Versuchsnummer const nameCorrection = { // map to right column names 'probennummer': 'samplenumber', 'name': 'materialname', 'firma': 'supplier', - 'reinforcingmaterial(contentin%)': 'reinforcingmaterial', 'teil/rohstoff': 'granulate/part', 'charge/batchgranulate/part': 'charge/batch', 'charge': 'charge/batch', @@ -120,7 +123,10 @@ async function importCsv(doc) { 'vz[ml/g]': 'vz(ml/g)', 'vz[cm³/g]': 'vz(ml/g)', 'abbau(%)': 'degradation(%)', - 'verstärkungsstoffgehalt(%)': 'glassfibrecontent(%)' + 'glassfibrecontent': 'reinforcingmaterialcontent', + 'glasfibrecontent(%)': 'reinforcingmaterialcontent', + 'reinforcingmaterial(contentin%)': 'reinforcingmaterialcontent', + 'verstärkungsstoffgehalt(%)': 'reinforcingmaterialcontent' }; const missingFieldsFill = [ // column names to fill if they do not exist 'color', @@ -129,7 +135,7 @@ async function importCsv(doc) { 'materialnumber', 'reinforcementmaterial' ] - console.log('importing ' + doc); + console.info('importing ' + doc); data = []; await new Promise(resolve => { fs.createReadStream(doc) @@ -158,9 +164,9 @@ async function importCsv(doc) { newE[field] = ''; } }); - // if(newE['materialname'] === '') { // TODO: is this replacement okay? - // newE['materialname'] = newE['material']; - // } + if(newE['materialname'] === '') { + newE['materialname'] = newE['material']; + } if (newE['supplier'] === '') { // empty supplier fields newE['supplier'] = 'unknown'; } @@ -211,12 +217,69 @@ async function allDpts() { res.data.forEach(sample => { sampleIds[sample.number] = sample._id; }); - const dptRegex = /(.*?)_(.*?)_(\d+|[a-zA-Z0-9]+_\d+).DPT/; + const dptRegex = /(.*?)_(.*?)_(\d+|[a-zA-Z0-9]+[_.]\d+)(_JDX)?[.]{1,2}(DPT|csv|CSV|JDX)/; const dpts = fs.readdirSync(dptFiles); for (let i in dpts) { - const regexRes = dptRegex.exec(dpts[i]) + let regexInput; + const bjRes = /^(Bj[FT]?)\s?([a-z0-9_]*)_JDX.DPT/.exec(dpts[i]); + if (bjRes) { // correct Bj numbers with space + regexInput = `Bj01_${bjRes[1]}${bjRes[2]}_0.DPT`; + } + else { // remove _JDX from name + regexInput = dpts[i].replace(/_JDX.*\./, '.'); + } + const regexRes = dptRegex.exec(regexInput); + if (regexRes && !sampleIds[regexRes[2]]) { // when sample number includes an additional _x instead of having _x_x for spectrum description + regexRes[2] = `${regexRes[2]}_${regexRes[3].split('_')[0]}`; + } + let baseSample = null; + if (regexRes) { + baseSample = regexRes[2].split('_')[0]; + if (baseSample === 'Wa11') { // as Wa11 samples use all the same material + baseSample = 'Wa11_B0_1'; + } + } + if (regexRes && !sampleIds[regexRes[2]] && sampleIds[baseSample]) { // when number_abx does not exist but number + dptSampleAddLog.push(`Trying to find ${baseSample}`); + dptSampleAddLog.push(host + '/sample/' + sampleIds[baseSample]); + res = await axios({ // get base sample + method: 'get', + url: host + '/sample/' + stripSpaces(sampleIds[baseSample]), + auth: { + username: 'admin', + password: 'Abc123!#' + } + }).catch(err => { + if (err.response) { + console.error(err.response.data); + errors.push(`DPT Could not fetch sample ${baseSample}: ${JSON.stringify(err.response.data)}`); + } + }); + if (res) { + const data = _.merge(_.pick(res.data, ['color', 'type', 'batch']), + {number: regexRes[2], condition: {}, notes: {}, material_id: res.data.material._id}); + res = await axios({ + method: 'post', + url: host + '/sample/new', + auth: { + username: res.data.user, + password: res.data.user === 'admin' ? 'Abc123!#' : '2020DeFinMachen!' + }, + data + }).catch(err => { + if (err.response) { + console.error(err.response.data); + errors.push(`DPT Could not save sample ${data}: ${err.response.data}`); + } + }); + if (res.data) { + dptSampleAddLog.push(`${regexRes[2]} from ${baseSample}`) + sampleIds[regexRes[2]] = res.data._id; + } + } + } if (regexRes && sampleIds[regexRes[2]]) { // found matching sample - console.log(`${dpts[i]} -> ${regexRes[2]}`); + console.log(`${i}/${dpts.length} ${dpts[i]} -> ${regexRes[2]}`); dptLog.push(`${dpts[i]}, ${regexRes[2]}`); const f = fs.readFileSync(dptFiles + '\\' + dpts[i], 'utf-8'); const data = { @@ -225,10 +288,11 @@ async function allDpts() { measurement_template }; data.values.device = regexRes[1]; - data.values.dpt = f.split('\r\n').map(e => e.split(',')); + data.values.filename = dpts[i]; + data.values.dpt = f.split('\r\n').map(e => e.split(',').map(e => parseFloat(e))); let rescale = false; for (let i in data.values.dpt) { - if (data.values.dpt[i][1] > 2) { + if (data.values.dpt[i][1] > 10) { rescale = true; break; } @@ -258,12 +322,17 @@ async function allDpts() { } else { console.log(`Could not find sample for ${dpts[i]}`); - errors.push(`Could not find sample for ${dpts[i]}`); + if (regexRes) { + errors.push(`Could not find sample for ${dpts[i]}; [DEBUG] ${regexRes[2]}, ${!sampleIds[regexRes[2]]}, ${sampleIds[baseSample]}`); + } + else { + errors.push(`Could not find sample for ${dpts[i]} (did not match RegEx)`); + } } } } -async function allKfVz() { +async function allMcVn() { let res = await axios({ method: 'get', url: host + '/template/measurements', @@ -272,8 +341,9 @@ async function allKfVz() { password: 'Abc123!#' } }); - const kf_template = res.data.filter(e => e.name === 'kf').sort((a, b) => b.version - a.version)[0]._id; - const vz_template = res.data.filter(e => e.name === 'vz').sort((a, b) => b.version - a.version)[0]._id; + const mc_template = res.data.filter(e => e.name === 'moisture content').sort((a, b) => b.version - a.version)[0]._id; + const vn_template = res.data.filter(e => e.name === 'vn').sort((a, b) => b.version - a.version)[0]._id; + const rmc_template = res.data.filter(e => e.name === 'reinforcement material content').sort((a, b) => b.version - a.version)[0]._id; res = await axios({ method: 'get', url: host + '/samples?status=all', @@ -287,14 +357,15 @@ async function allKfVz() { sampleIds[sample.number] = sample._id; }); for (let index in data) { - console.info(`KF/VZ ${index}/${data.length}`); + console.info(`MC/VN ${index}/${data.length}`); let sample = data[index]; + sample['samplenumber'] = sample['samplenumber'].replace(/[A-Z][a-z]0\d_/, ''); let credentials = ['admin', 'Abc123!#']; if (sampleDevices[sample['samplenumber']]) { credentials = [sampleDevices[sample['samplenumber']], '2020DeFinMachen!'] } - if (!sample['vz(ml/g)'] && vzValues[sample['samplenumber']]) { // fill in VZ values from comments - sample['vz(ml/g)'] = vzValues[sample['samplenumber']]; + if (!sample['vz(ml/g)'] && vnValues[sample['samplenumber']]) { // fill in VN values from comments + sample['vz(ml/g)'] = vnValues[sample['samplenumber']]; } if (sample['kfingew%']) { await axios({ @@ -306,7 +377,7 @@ async function allKfVz() { }, data: { sample_id: sampleIds[sample['samplenumber']], - measurement_template: kf_template, + measurement_template: mc_template, values: { 'weight %': sample['kfingew%'], 'standard deviation': sample['stabwn'] @@ -315,7 +386,7 @@ async function allKfVz() { }).catch(err => { console.log(sample['samplenumber']); console.error(err.response.data); - errors.push(`KF/VZ upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`); + errors.push(`MC/VN upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`); }); } if (sample['vz(ml/g)']) { @@ -328,15 +399,36 @@ async function allKfVz() { }, data: { sample_id: sampleIds[sample['samplenumber']], - measurement_template: vz_template, + measurement_template: vn_template, values: { - vz: sample['vz(ml/g)'] + vn: sample['vz(ml/g)'] } } }).catch(err => { console.log(sample['samplenumber']); console.error(err.response.data); - errors.push(`KF/VZ upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`); + errors.push(`MC/VN upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`); + }); + } + if (sample['reinforcingmaterialcontent']) { + await axios({ + method: 'post', + url: host + '/measurement/new', + auth: { + username: credentials[0], + password: credentials[1] + }, + data: { + sample_id: sampleIds[sample['samplenumber']], + measurement_template: rmc_template, + values: { + percentage: Number(sample['reinforcingmaterialcontent'].replace('%', '').replace(',', '.')) + } + } + }).catch(err => { + console.log(sample['samplenumber']); + console.error(err.response.data); + errors.push(`MC/VN upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`); }); } } @@ -422,7 +514,7 @@ async function allSamples() { samples[si].color = number.color; } } - else if (sampleColors[sample['samplenumber'].split('_')[0]]) { // derive color from main sample for kf/vz + else if (sampleColors[sample['samplenumber'].split('_')[0]]) { // derive color from main sample for mc/vn samples[si].color = sampleColors[sample['samplenumber'].split('_')[0]]; } if (!samples[si].color) { @@ -436,7 +528,6 @@ async function saveSamples() { console.info(`SAMPLE SAVE ${i}/${samples.length}`); let credentials = ['admin', 'Abc123!#']; if (sampleDevices[samples[i].number]) { - console.log(sampleDevices[samples[i].number]); credentials = [sampleDevices[samples[i].number], '2020DeFinMachen!'] } await axios({ @@ -520,7 +611,7 @@ async function allMaterials() { password: 'Abc123!#' } }); - const materialTemplate = res.data.find(e => e.name === 'plastic')._id; + const materialTemplate = res.data.filter(e => e.name === 'plastic').sort((a, b) => b.version - a.version)[0]._id; // process all samples for (let index in data) { @@ -718,7 +809,7 @@ function readPdf(file) { let lastLastText = ''; // text of last last item await new pdfReader.PdfReader().parseFileItems(nmDocs + '\\' + file, (err, item) => { if (item && item.text) { - if ((stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignationsuppl') >= 0) || (stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignatiomsupplier') >= 0)) { // table area starts + if ((stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignationsuppl') >= 0) || (stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignatiomsuppl') >= 0)) { // table area starts table = countdown; } if (table > 0) { @@ -822,9 +913,9 @@ function customFields (comment, sampleNumber) { {docKey: 'zu', dbKey: 'belongs to', regex: /zu (\S*\d+)/, category: 'reference'}, {docKey: 'granulate zu', dbKey: 'granulate to', regex: /granulate zu.* (\S*\d+)/, category: 'reference'}, {docKey: 'construction part', dbKey: 'construction part', regex: /(? i > 0).join(' '); @@ -864,8 +955,12 @@ function customFields (comment, sampleNumber) { } function sampleType (type) { - const allowedTypes = ['tension rod', 'part', 'granulate']; - return allowedTypes.indexOf(type) >= 0 ? type : (type === '' ? 'unknown' : 'other'); + type = stripSpaces(type).toLowerCase(); + const allowedTypes = {'tension rod': 'tension rod', 'Zugstab': 'tension rod', 'part': 'part', 'granulate': 'granulate'}; + if (!allowedTypes[type]) { + typeLog.push(type); + } + return allowedTypes[type] ? allowedTypes[type] : 'part'; } function stripSpaces(s) { diff --git a/package-lock.json b/package-lock.json index 6749f0a..eb753e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2522,11 +2522,6 @@ } } }, - "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 7bf20ea..e42f017 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "mocha dist/**/**.spec.js", "start": "node index.js", "dev": "nodemon -e ts,yaml --exec \"tsc && node dist/index.js || exit 1\"", + "start-local": "node dist/index.js", "loadDev": "node dist/test/loadDev.js", "coverage": "tsc && nyc --reporter=html --reporter=text mocha dist/**/**.spec.js --timeout 5000", "import": "node data_import/import.js" @@ -35,7 +36,6 @@ "json-schema": "^0.2.5", "json2csv": "^5.0.1", "lodash": "^4.17.15", - "mongo-sanitize": "^1.1.0", "mongoose": "^5.8.7", "swagger-ui-dist": "^3.30.2" }, diff --git a/src/db.ts b/src/db.ts index 2bab005..8f72f81 100644 --- a/src/db.ts +++ b/src/db.ts @@ -7,7 +7,7 @@ import ChangelogModel from './models/changelog'; // database urls, prod db url is retrieved automatically const TESTING_URL = 'mongodb://localhost/dfopdb_test'; const DEV_URL = 'mongodb://localhost/dfopdb'; -const debugging = true; +const debugging = false; if (process.env.NODE_ENV !== 'production' && debugging) { mongoose.set('debug', true); // enable mongoose debug @@ -114,6 +114,9 @@ export default class db { Object.keys(json.collections).forEach(collectionName => { // create each collection json.collections[collectionName] = this.oidResolve(json.collections[collectionName]); this.state.db.createCollection(collectionName, (err, collection) => { + if (err) { + console.error(err); + } collection.insertMany(json.collections[collectionName], () => { // insert JSON data if (++ loadCounter >= Object.keys(json.collections).length) { // all collections loaded done(); diff --git a/src/globals.ts b/src/globals.ts index 81f80b8..f51bf57 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -1,8 +1,7 @@ const globals = { - levels: [ // access levels + levels: [ // access levels, sorted asc by rights 'read', 'write', - 'maintain', 'dev', 'admin' ], diff --git a/src/helpers/mail.ts b/src/helpers/mail.ts index 8ec71c8..901c5f5 100644 --- a/src/helpers/mail.ts +++ b/src/helpers/mail.ts @@ -2,44 +2,65 @@ import axios from 'axios'; // sends an email using the BIC service -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: "definma@bosch-iot.com", - password: "PlasticsOfFingerprintDigital" - } - } - }) - .then(() => { - f(); - }) - .catch((err) => { - f(err); - }); - } - else if (process.env.NODE_ENV === 'test') { - console.info('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content); - f(); - } - else { // dev - axios({ +export default class Mail{ + + static readonly address = 'definma@bosch-iot.com'; + static uri: string; + static auth = {username: '', password: ''}; + static mailPass: string; + + static init() { + this.mailPass = Array(64).map(() => Math.floor(Math.random() * 10)).join(''); + this.uri = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.uri; + this.auth.username = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.username; + this.auth.password = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.password; + axios({ // get registered mail addresses method: 'get', - url: 'https://digital-fingerprint-of-plastics-mail-test.apps.de1.bosch-iot-cloud.com/api', - data: { + url: this.uri + '/management/userDomainMapping', + auth: this.auth + }).then(res => { + return new Promise(async (resolve, reject) => { + try { + if (res.data.addresses.indexOf(this.address) < 0) { // mail address not registered + if (res.data.addresses.length) { // delete wrong registered mail address + await axios({ + method: 'delete', + url: this.uri + '/management/mailAddresses/' + res.data.addresses[0], + auth: this.auth + }); + } + await axios({ // register right mail address + method: 'post', + url: this.uri + '/management/mailAddresses/' + this.address, + auth: this.auth + }); + } + resolve(); + } + catch (e) { + reject(e); + } + }); + }).then(() => { + return axios({ // set new mail password + method: 'put', + url: this.uri + '/management/mailAddresses/' + this.address + '/password/' + this.mailPass, + auth: this.auth + }); + }).then(() => { // init done successfully + this.send('lukas.veit@bosch.com', 'Mail Service started', new Date().toString()); + }).catch(err => { // anywhere an error occurred + console.error(`Mail init error: ${err.request.method} ${err.request.path}: ${err.response.status}`, + err.response.data); + }); + } + + static send (mailAddress, subject, content, f = () => {}) { // callback, executed empty or with error + if (process.env.NODE_ENV === 'production') { // only send mails in production + axios({ method: 'post', - url: '/email', + url: this.uri + '/email', + auth: this.auth, data: { recipients: [{to: mailAddress}], subject: {content: subject}, @@ -48,17 +69,19 @@ export default (mailAddress, subject, content, f) => { // callback, executed em contentType: "text/html" }, from: { - eMail: "dfop-test@bosch-iot.com", - password: "PlasticsOfFingerprintDigital" + eMail: this.address, + password: this.mailPass } } - } - }) - .then(() => { + }).then(() => { f(); - }) - .catch((err) => { + }).catch((err) => { f(err); }); + } + else { // dev dummy replacement + console.info('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content); + f(); + } } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 3fc4ef8..54f3c68 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ import express from 'express'; import bodyParser from 'body-parser'; import compression from 'compression'; import contentFilter from 'content-filter'; -import mongoSanitize from 'mongo-sanitize'; import helmet from 'helmet'; import cors from 'cors'; import api from './api'; @@ -11,7 +10,8 @@ import db from './db'; // TODO: check header, also in UI // tell if server is running in debug or production environment -console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT ====='); +console.info(process.env.NODE_ENV === 'production' ? + '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT ====='); // mongodb connection @@ -61,15 +61,15 @@ app.use('/static/img/bosch-logo.svg', helmet.contentSecurityPolicy({ })); // middleware -app.use(contentFilter()); // filter URL query attacks +app.use(compression()); // compress responses app.use(express.json({ limit: '5mb'})); app.use(express.urlencoded({ extended: false, limit: '5mb' })); -app.use(compression()); // compress responses app.use(bodyParser.json()); -app.use((req, res, next) => { // filter body query attacks - req.body = mongoSanitize(req.body); - next(); -}); +const injectionBlackList = ['$', '{', '&&', '||']; +app.use(contentFilter({ + urlBlackList: injectionBlackList, + bodyBlackList: injectionBlackList +})); // filter URL query attacks app.use((err, req, res, ignore) => { // bodyParser error handling res.status(400).send({status: 'Invalid JSON body'}); }); diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts index 789f6e5..5d60983 100644 --- a/src/routes/material.spec.ts +++ b/src/routes/material.spec.ts @@ -208,7 +208,7 @@ describe('/material', () => { res: {_id: '100000000000000000000007', name: 'Ultramid A4H', supplier: 'BASF', group: 'PA66', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 0}, numbers: []} }); }); - it('returns a deleted material for a maintain/admin user', done => { + it('returns a deleted material for a dev/admin user', done => { TestHelper.request(server, done, { method: 'get', url: '/material/100000000000000000000008', diff --git a/src/routes/material.ts b/src/routes/material.ts index 54a49ab..dfd7cbf 100644 --- a/src/routes/material.ts +++ b/src/routes/material.ts @@ -19,7 +19,7 @@ import ParametersValidate from './validate/parameters'; const router = express.Router(); router.get('/materials', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; const {error, value: filters} = MaterialValidate.query(req.query); if (error) return res400(error, res); @@ -41,22 +41,25 @@ router.get('/materials', (req, res, next) => { MaterialModel.find(conditions).populate('group_id').populate('supplier_id').lean().exec((err, data) => { if (err) return next(err); - res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => MaterialValidate.output(e)))); }); }); router.get('/materials/:state(new|deleted)', (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - MaterialModel.find({status: globals.status[req.params.state]}).populate('group_id').populate('supplier_id').lean().exec((err, data) => { + MaterialModel.find({status: globals.status[req.params.state]}).populate('group_id').populate('supplier_id') + .lean().exec((err, data) => { if (err) return next(err); - res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => MaterialValidate.output(e)))); }); }); router.get('/material/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; MaterialModel.findById(req.params.id).populate('group_id').populate('supplier_id').lean().exec((err, data: any) => { if (err) return next(err); @@ -65,13 +68,14 @@ router.get('/material/' + IdValidate.parameter(), (req, res, next) => { return res.status(404).json({status: 'Not found'}); } - if (data.status === globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted materials only available for maintain/admin + // deleted materials only available for dev/admin + if (data.status === globals.status.deleted && !req.auth(res, ['dev', 'admin'], 'all')) return; res.json(MaterialValidate.output(data)); }); }); router.put('/material/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; let {error, value: material} = MaterialValidate.input(req.body, 'change'); if (error) return res400(error, res); @@ -95,7 +99,8 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => { if (!material) return; } if (material.hasOwnProperty('properties')) { - if (!await propertiesCheck(material.properties, 'change', res, next, materialData.properties.material_template.toString() !== material.properties.material_template)) return; + if (!await propertiesCheck(material.properties, 'change', res, next, + materialData.properties.material_template.toString() !== material.properties.material_template)) return; } // check for changes @@ -103,7 +108,8 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => { material.status = globals.status.new; // set status to new } - await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => { + await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}) + .log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => { if (err) return next(err); res.json(MaterialValidate.output(data)); }); @@ -111,7 +117,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => { }); router.delete('/material/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; // check if there are still samples referencing this material SampleModel.find({'material_id': new mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => { @@ -119,7 +125,8 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => { if (data.length) { return res.status(400).json({status: 'Material still in use'}); } - MaterialModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => { + MaterialModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}) + .log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => { if (err) return next(err); if (data) { res.json({status: 'OK'}); @@ -132,19 +139,19 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => { }); router.put('/material/restore/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; setStatus(globals.status.new, req, res, next); }); router.put('/material/validate/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; setStatus(globals.status.validated, req, res, next); }); router.post('/material/new', async (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; let {error, value: material} = MaterialValidate.input(req.body, 'new'); if (error) return res400(error, res); @@ -167,22 +174,24 @@ router.post('/material/new', async (req, res, next) => { }); router.get('/material/groups', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; MaterialGroupModel.find().lean().exec((err, data: any) => { if (err) return next(err); - res.json(_.compact(data.map(e => MaterialValidate.outputGroups(e.name)))); // validate all and filter null values from validation errors + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => MaterialValidate.outputGroups(e.name)))); }); }); router.get('/material/suppliers', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; MaterialSupplierModel.find().lean().exec((err, data: any) => { if (err) return next(err); - res.json(_.compact(data.map(e => MaterialValidate.outputSuppliers(e.name)))); // validate all and filter null values from validation errors + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => MaterialValidate.outputSuppliers(e.name)))); }); }); @@ -201,7 +210,11 @@ async function nameCheck (material, res, next) { // check if name was already t } async function groupResolve (material, req, next) { - const groupData = await MaterialGroupModel.findOneAndUpdate({name: material.group}, {name: material.group}, {upsert: true, new: true}).log(req).lean().exec().catch(err => next(err)) as any; + const groupData = await MaterialGroupModel.findOneAndUpdate( + {name: material.group}, + {name: material.group}, + {upsert: true, new: true} + ).log(req).lean().exec().catch(err => next(err)) as any; if (groupData instanceof Error) return false; material.group_id = groupData._id; delete material.group; @@ -209,19 +222,25 @@ async function groupResolve (material, req, next) { } async function supplierResolve (material, req, next) { - const supplierData = await MaterialSupplierModel.findOneAndUpdate({name: material.supplier}, {name: material.supplier}, {upsert: true, new: true}).log(req).lean().exec().catch(err => next(err)) as any; + const supplierData = await MaterialSupplierModel.findOneAndUpdate( + {name: material.supplier}, + {name: material.supplier}, + {upsert: true, new: true} + ).log(req).lean().exec().catch(err => next(err)) as any; if (supplierData instanceof Error) return false; material.supplier_id = supplierData._id; delete material.supplier; return material; } -async function propertiesCheck (properties, param, res, next, checkVersion = true) { // validate material properties, returns false if invalid, otherwise template data +// validate material properties, returns false if invalid, otherwise template data +async function propertiesCheck (properties, param, res, next, checkVersion = true) { if (!properties.material_template || !IdValidate.valid(properties.material_template)) { // template id not found res.status(400).json({status: 'Material template not available'}); return false; } - const materialData = await MaterialTemplateModel.findById(properties.material_template).lean().exec().catch(err => next(err)) as any; + const materialData = await MaterialTemplateModel.findById(properties.material_template) + .lean().exec().catch(err => next(err)) as any; if (materialData instanceof Error) return false; if (!materialData) { // template not found res.status(400).json({status: 'Material template not available'}); @@ -230,7 +249,8 @@ async function propertiesCheck (properties, param, res, next, checkVersion = tru if (checkVersion) { // get all template versions and check if given is latest - const materialVersions = await MaterialTemplateModel.find({first_id: materialData.first_id}).sort({version: -1}).lean().exec().catch(err => next(err)) as any; + const materialVersions = await MaterialTemplateModel.find({first_id: materialData.first_id}).sort({version: -1}) + .lean().exec().catch(err => next(err)) as any; if (materialVersions instanceof Error) return false; if (properties.material_template !== materialVersions[0]._id.toString()) { // template not latest res.status(400).json({status: 'Old template version not allowed'}); @@ -239,7 +259,8 @@ async function propertiesCheck (properties, param, res, next, checkVersion = tru } // validate parameters - const {error, value} = ParametersValidate.input(_.omit(properties, 'material_template'), materialData.parameters, param); + const {error, value} = ParametersValidate + .input(_.omit(properties, 'material_template'), materialData.parameters, param); if (error) {res400(error, res); return false;} Object.keys(value).forEach(key => { properties[key] = value[key]; diff --git a/src/routes/measurement.spec.ts b/src/routes/measurement.spec.ts index 180f3ce..cf01acb 100644 --- a/src/routes/measurement.spec.ts +++ b/src/routes/measurement.spec.ts @@ -16,7 +16,7 @@ describe('/measurement', () => { TestHelper.request(server, done, { method: 'get', url: '/measurement/800000000000000000000001', - auth: {basic: 'janedoe'}, + auth: {basic: 'admin'}, httpStatus: 200, res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'} }); @@ -25,12 +25,21 @@ describe('/measurement', () => { TestHelper.request(server, done, { method: 'get', url: '/measurement/800000000000000000000001', - auth: {key: 'janedoe'}, + auth: {key: 'admin'}, httpStatus: 200, res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'} }); }); - it('returns deleted measurements for a maintain/admin user', done => { + it('filters out spectral data for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/800000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I'}, measurement_template: '300000000000000000000001'} + }); + }); + it('returns deleted measurements for a dev/admin user', done => { TestHelper.request(server, done, { method: 'get', url: '/measurement/800000000000000000000004', @@ -77,7 +86,7 @@ describe('/measurement', () => { TestHelper.request(server, done, { method: 'put', url: '/measurement/800000000000000000000001', - auth: {basic: 'janedoe'}, + auth: {basic: 'admin'}, httpStatus: 200, req: {}, res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'} @@ -87,7 +96,7 @@ describe('/measurement', () => { TestHelper.request(server, done, { method: 'put', url: '/measurement/800000000000000000000001', - auth: {basic: 'janedoe'}, + auth: {basic: 'admin'}, httpStatus: 200, req: {values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}} }).end((err, res) => { @@ -121,7 +130,7 @@ describe('/measurement', () => { TestHelper.request(server, done, { method: 'put', url: '/measurement/800000000000000000000001', - auth: {basic: 'janedoe'}, + auth: {basic: 'admin'}, httpStatus: 200, req: {values: {dpt: [[1,2],[3,4],[5,6]]}} }).end((err, res) => { @@ -244,7 +253,7 @@ describe('/measurement', () => { req: {values: {val1: 2}} }); }); - it('accepts editing a measurement of another user for a maintain/admin user', done => { + it('accepts editing a measurement of another user for a dev/admin user', done => { TestHelper.request(server, done, { method: 'put', url: '/measurement/800000000000000000000002', @@ -362,7 +371,7 @@ describe('/measurement', () => { httpStatus: 403, }); }); - it('accepts deleting a measurement of another user for a maintain/admin user', done => { + it('accepts deleting a measurement of another user for a dev/admin user', done => { TestHelper.request(server, done, { method: 'delete', url: '/measurement/800000000000000000000001', @@ -731,7 +740,7 @@ describe('/measurement', () => { req: {sample_id: '400000000000000000000003', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} }); }); - it('accepts adding a measurement to the sample of another user for a maintain/admin user', done => { + it('accepts adding a measurement to the sample of another user for a dev/admin user', done => { TestHelper.request(server, done, { method: 'post', url: '/measurement/new', diff --git a/src/routes/measurement.ts b/src/routes/measurement.ts index 47af305..5078379 100644 --- a/src/routes/measurement.ts +++ b/src/routes/measurement.ts @@ -15,21 +15,22 @@ import db from '../db'; const router = express.Router(); router.get('/measurement/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; MeasurementModel.findById(req.params.id).lean().exec((err, data: any) => { if (err) return next(err); if (!data) { return res.status(404).json({status: 'Not found'}); } - if (data.status ===globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted measurements only available for maintain/admin + // deleted measurements only available for dev/admin + if (data.status === globals.status.deleted && !req.auth(res, ['dev', 'admin'], 'all')) return; - res.json(MeasurementValidate.output(data)); + res.json(MeasurementValidate.output(data, req)); }); }); router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; const {error, value: measurement} = MeasurementValidate.input(req.body, 'change'); if (error) return res400(error, res); @@ -57,14 +58,15 @@ router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => { } if (!await templateCheck(measurement, 'change', res, next)) return; - await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}).log(req).lean().exec((err, data) => { + await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}) + .log(req).lean().exec((err, data) => { if (err) return next(err); - res.json(MeasurementValidate.output(data)); + res.json(MeasurementValidate.output(data, req)); }); }); router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; MeasurementModel.findById(req.params.id).lean().exec(async (err, data) => { if (err) return next(err); @@ -72,7 +74,8 @@ router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => { return res.status(404).json({status: 'Not found'}); } if (!await sampleIdCheck(data, req, res, next)) return; - await MeasurementModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).lean().exec(err => { + await MeasurementModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}) + .log(req).lean().exec(err => { if (err) return next(err); return res.json({status: 'OK'}); }); @@ -80,19 +83,19 @@ router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => { }); router.put('/measurement/restore/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; setStatus(globals.status.new, req, res, next); }); router.put('/measurement/validate/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; setStatus(globals.status.validated, req, res, next); }); router.post('/measurement/new', async (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; const {error, value: measurement} = MeasurementValidate.input(req.body, 'new'); if (error) return res400(error, res); @@ -105,7 +108,7 @@ router.post('/measurement/new', async (req, res, next) => { await new MeasurementModel(measurement).save((err, data) => { if (err) return next(err); db.log(req, 'measurements', {_id: data._id}, data.toObject()); - res.json(MeasurementValidate.output(data.toObject())); + res.json(MeasurementValidate.output(data.toObject(), req)); }); }); @@ -113,18 +116,23 @@ router.post('/measurement/new', async (req, res, next) => { module.exports = router; -async function sampleIdCheck (measurement, req, res, next) { // validate sample_id, returns false if invalid or user has no access for this sample - const sampleData = await SampleModel.findById(measurement.sample_id).lean().exec().catch(err => {next(err); return false;}) as any; +// validate sample_id, returns false if invalid or user has no access for this sample +async function sampleIdCheck (measurement, req, res, next) { + const sampleData = await SampleModel.findById(measurement.sample_id) + .lean().exec().catch(err => {next(err); return false;}) as any; if (!sampleData) { // sample_id not found res.status(400).json({status: 'Sample id not available'}); return false } - if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return false; // sample does not belong to user - return true; + // sample does not belong to user + return !(sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')); } -async function templateCheck (measurement, param, res, next) { // validate measurement_template and values, returns values, true if values are {} or false if invalid, param for 'new'/'change' - const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template).lean().exec().catch(err => {next(err); return false;}) as any; +// validate measurement_template and values, returns values, true if values are {} or false if invalid, +// param for 'new'/'change' +async function templateCheck (measurement, param, res, next) { + const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template) + .lean().exec().catch(err => {next(err); return false;}) as any; if (!templateData) { // template not found res.status(400).json({status: 'Measurement template not available'}); return false @@ -133,7 +141,8 @@ async function templateCheck (measurement, param, res, next) { // validate meas // fill not given values for new measurements if (param === 'new') { // get all template versions and check if given is latest - const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1}).lean().exec().catch(err => next(err)) as any; + const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1}) + .lean().exec().catch(err => next(err)) as any; if (templateVersions instanceof Error) return false; if (measurement.measurement_template !== templateVersions[0]._id.toString()) { // template not latest res.status(400).json({status: 'Old template version not allowed'}); diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index b84d0c2..8ce7564 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -179,7 +179,7 @@ describe('/', () => { url: '/authorized', auth: {key: 'admin'}, httpStatus: 200, - res: {status: 'Authorization successful', method: 'key', level: 'admin'} + res: {status: 'Authorization successful', method: 'key', level: 'admin', user_id: '000000000000000000000003'} }); }); it('works with basic auth', done => { @@ -188,7 +188,7 @@ describe('/', () => { url: '/authorized', auth: {basic: 'admin'}, httpStatus: 200, - res: {status: 'Authorization successful', method: 'basic', level: 'admin'} + res: {status: 'Authorization successful', method: 'basic', level: 'admin', user_id: '000000000000000000000003'} }); }); }); @@ -207,17 +207,17 @@ describe('/', () => { }); }); - describe('A not connected database', () => { // RUN AS LAST OR RECONNECT DATABASE!! - it('resolves to an 500 error', done => { - db.disconnect(() => { - TestHelper.request(server, done, { - method: 'get', - url: '/', - httpStatus: 500 - }); - }); - }); - }); + // describe('A not connected database', () => { // RUN AS LAST OR RECONNECT DATABASE!! + // it('resolves to an 500 error', done => { + // db.disconnect(() => { + // TestHelper.request(server, done, { + // method: 'get', + // url: '/', + // httpStatus: 500 + // }); + // }); + // }); + // }); }); describe('The /api/{url} redirect', () => { @@ -242,15 +242,15 @@ describe('The /api/{url} redirect', () => { url: '/api/authorized', auth: {basic: 'admin'}, httpStatus: 200, - res: {status: 'Authorization successful', method: 'basic', level: 'admin'} - }); - }); - it('is disabled in production', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/api/authorized', - auth: {basic: 'admin'}, - httpStatus: 404 + res: {status: 'Authorization successful', method: 'basic', level: 'admin', user_id: '000000000000000000000003'} }); }); + // it('is disabled in production', done => { + // TestHelper.request(server, done, { + // method: 'get', + // url: '/api/authorized', + // auth: {basic: 'admin'}, + // httpStatus: 404 + // }); + // }); }); \ No newline at end of file diff --git a/src/routes/root.ts b/src/routes/root.ts index 20f10b9..bc0fc98 100644 --- a/src/routes/root.ts +++ b/src/routes/root.ts @@ -14,21 +14,33 @@ router.get('/', (req, res) => { router.get('/authorized', (req, res) => { if (!req.auth(res, globals.levels)) return; - res.json({status: 'Authorization successful', method: req.authDetails.method, level: req.authDetails.level}); + res.json({ + status: 'Authorization successful', + method: req.authDetails.method, + level: req.authDetails.level, + user_id: req.authDetails.id + }); }); -// TODO: evaluate exact changelog functionality (restoring, delting after time, etc.) +// TODO: evaluate exact changelog functionality (restoring, deleting after time, etc.) router.get('/changelog/:timestamp/:page?/:pagesize?', (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - const {error, value: options} = RootValidate.changelogParams({timestamp: req.params.timestamp, page: req.params.page, pagesize: req.params.pagesize}); + const {error, value: options} = RootValidate.changelogParams({ + timestamp: req.params.timestamp, + page: req.params.page, + pagesize: req.params.pagesize + }); if (error) return res400(error, res); - const id = new mongoose.Types.ObjectId(Math.floor(new Date(options.timestamp).getTime() / 1000).toString(16) + '0000000000000000'); - ChangelogModel.find({_id: {$lte: id}}).sort({_id: -1}).skip(options.page * options.pagesize).limit(options.pagesize).lean().exec((err, data) => { + const id = new mongoose.Types + .ObjectId(Math.floor(new Date(options.timestamp).getTime() / 1000).toString(16) + '0000000000000000'); + ChangelogModel.find({_id: {$lte: id}}).sort({_id: -1}).skip(options.page * options.pagesize).limit(options.pagesize) + .lean().exec((err, data) => { if (err) return next(err); - res.json(_.compact(data.map(e => RootValidate.changelogOutput(e)))); // validate all and filter null values from validation errors + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => RootValidate.changelogOutput(e)))); }); }); diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts index d9db97a..a7cd2d5 100644 --- a/src/routes/sample.spec.ts +++ b/src/routes/sample.spec.ts @@ -262,7 +262,7 @@ describe('/sample', () => { TestHelper.request(server, done, { method: 'get', url: '/samples?status=all&fields[]=number&fields[]=measurements.spectrum.dpt', - auth: {basic: 'janedoe'}, + auth: {basic: 'admin'}, httpStatus: 200 }).end((err, res) => { if (err) return done(err); @@ -379,6 +379,14 @@ describe('/sample', () => { done(); }); }); + it('rejects returning spectral data for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&fields[]=number&fields[]=measurements.spectrum.dpt', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); it('rejects an invalid JSON string as a filters parameter', done => { TestHelper.request(server, done, { method: 'get', @@ -681,7 +689,25 @@ describe('/sample', () => { res: {_id: '400000000000000000000003', number: '33', type: 'part', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'granulate to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], user: 'admin'} }); }); - it('returns a deleted sample for a maintain/admin user', done => { + it ('filters out spectral data for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {device: 'Alpha II'}, measurement_template: '300000000000000000000001'}]} + }); + }); + it ('returns spectral data for an admin user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[ 3997.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]],device: 'Alpha I'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II'}, measurement_template: '300000000000000000000001'}]} + }); + }); + it('returns a deleted sample for a dev/admin user', done => { TestHelper.request(server, done, { method: 'get', url: '/sample/400000000000000000000005', @@ -1054,6 +1080,16 @@ describe('/sample', () => { res: {status: 'Condition template not available'} }); }); + it('rejects a not accepted type', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {type: 'xx'}, + res: {status: 'Invalid body format', details: '"type" must be one of [granulate, part, tension rod]'} + }); + }); it('allows keeping an empty condition empty', done => { TestHelper.request(server, done, { method: 'put', @@ -1121,7 +1157,7 @@ describe('/sample', () => { req: {} }); }); - it('accepts changes for samples from another user for a maintain/admin user', done => { + it('accepts changes for samples from another user for a dev/admin user', done => { TestHelper.request(server, done, { method: 'put', url: '/sample/400000000000000000000001', @@ -1260,7 +1296,7 @@ describe('/sample', () => { }); }); - it('lets admin/maintain users delete samples of other users', done => { + it('lets admin/dev users delete samples of other users', done => { TestHelper.request(server, done, { method: 'delete', url: '/sample/400000000000000000000001', @@ -1362,7 +1398,7 @@ describe('/sample', () => { res: {_id: '400000000000000000000003', number: '33', type: 'part', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'granulate to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], user: 'admin'} }); }); - it('returns a deleted sample for a maintain/admin user', done => { + it('returns a deleted sample for a dev/admin user', done => { TestHelper.request(server, done, { method: 'get', url: '/sample/number/Rng33', @@ -1371,6 +1407,24 @@ describe('/sample', () => { res: {_id: '400000000000000000000005', number: 'Rng33', type: 'granulate', color: 'black', batch: '1653000308', condition: {condition_template: '200000000000000000000003'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {}, measurements: [], user: 'admin'} }); }); + it ('filters out spectral data for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/1', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {device: 'Alpha II'}, measurement_template: '300000000000000000000001'}]} + }); + }); + it ('returns spectral data for an admin user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/1', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[ 3997.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]],device: 'Alpha I'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II'}, measurement_template: '300000000000000000000001'}]} + }); + }); it('returns 403 for a write user when requesting a deleted sample', done => { TestHelper.request(server, done, { method: 'get', @@ -1513,24 +1567,38 @@ describe('/sample', () => { } }); }); - it('rejects validating a sample without condition', done => { + it('allows validating a sample without condition', done => { TestHelper.request(server, done, { method: 'put', url: '/sample/validate/400000000000000000000006', auth: {basic: 'admin'}, - httpStatus: 400, - req: {}, - res: {status: 'Sample without condition cannot be valid'} + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + SampleModel.findById('400000000000000000000006').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status',globals.status.validated); + done(); + }); }); }); - it('rejects validating a sample without measurements', done => { + it('allows validating a sample without measurements', done => { TestHelper.request(server, done, { method: 'put', url: '/sample/validate/400000000000000000000004', auth: {basic: 'admin'}, - httpStatus: 400, - req: {}, - res: {status: 'Sample without measurements cannot be valid'} + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + SampleModel.findById('400000000000000000000004').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status',globals.status.validated); + done(); + }); }); }); it('rejects an API key', done => { @@ -1937,6 +2005,16 @@ describe('/sample', () => { res: {status: 'Invalid body format', details: 'Invalid object id'} }); }); + it('rejects a not accepted type', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'xx', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment'}}, + res: {status: 'Invalid body format', details: '"type" must be one of [granulate, part, tension rod]'} + }); + }); it('rejects an API key', done => { TestHelper.request(server, done, { method: 'post', diff --git a/src/routes/sample.ts b/src/routes/sample.ts index 10694ac..9b93aaa 100644 --- a/src/routes/sample.ts +++ b/src/routes/sample.ts @@ -28,14 +28,19 @@ const router = express.Router(); // TODO: think about filter keys with measurement template versions + router.get('/samples', async (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; const {error, value: filters} = SampleValidate.query(req.query); if (error) return res400(error, res); + // spectral data not allowed for read/write users + if (filters.fields.find(e => /\.dpt$/.test(e)) && !req.auth(res, ['dev', 'admin'], 'all')) return; + // TODO: find a better place for these - const sampleKeys = ['_id', 'color', 'number', 'type', 'batch', 'added', 'condition', 'material_id', 'note_id', 'user_id']; + const sampleKeys = ['_id', 'color', 'number', 'type', 'batch', 'added', 'condition', 'material_id', 'note_id', + 'user_id']; // evaluate sort parameter from 'color-asc' to ['color', 1] filters.sort = filters.sort.split('-'); @@ -74,7 +79,8 @@ router.get('/samples', async (req, res, next) => { } else { // start and end of day - const date = [new Date(addedFilter.values[0]).setHours(0,0,0,0), new Date(addedFilter.values[0]).setHours(23,59,59,999)]; + const date = [new Date(addedFilter.values[0]).setHours(0,0,0,0), + new Date(addedFilter.values[0]).setHours(23,59,59,999)]; if (addedFilter.mode === 'lt') { // lt start filters.filters.push({mode: 'lt', field: '_id', values: [dateToOId(date[0])]}); } @@ -88,7 +94,8 @@ router.get('/samples', async (req, res, next) => { filters.filters.push({mode: 'gte', field: '_id', values: [dateToOId(date[0])]}); } if (addedFilter.mode === 'ne') { - filters.filters.push({mode: 'or', field: '_id', values: [{ _id: { '$lt': dateToOId(date[0])}}, { _id: { '$gt': dateToOId(date[1])}}]}); + filters.filters.push({mode: 'or', field: '_id', + values: [{ _id: { '$lt': dateToOId(date[0])}}, { _id: { '$gt': dateToOId(date[1])}}]}); } } } @@ -103,27 +110,31 @@ router.get('/samples', async (req, res, next) => { if (filters.sort[0].indexOf('measurements.') >= 0) { // sorting with measurements as starting collection collection = MeasurementModel; const [,measurementName, measurementParam] = filters.sort[0].split('.'); - const measurementTemplates = await MeasurementTemplateModel.find({name: measurementName}).lean().exec().catch(err => {next(err);}); + const measurementTemplates = await MeasurementTemplateModel.find({name: measurementName}) + .lean().exec().catch(err => {next(err);}); if (measurementTemplates instanceof Error) return; if (!measurementTemplates) { return res.status(400).json({status: 'Invalid body format', details: filters.sort[0] + ' not found'}); } let sortStartValue = null; if (filters['from-id']) { // from-id specified, fetch values for sorting - const fromSample = await MeasurementModel.findOne({sample_id: mongoose.Types.ObjectId(filters['from-id'])}).lean().exec().catch(err => {next(err);}); // TODO: what if more than one measurement for sample? + const fromSample = await MeasurementModel.findOne({sample_id: mongoose.Types.ObjectId(filters['from-id'])}) + .lean().exec().catch(err => {next(err);}); // TODO: what if more than one measurement for sample? if (fromSample instanceof Error) return; if (!fromSample) { return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'}); } sortStartValue = fromSample.values[measurementParam]; } - queryPtr[0].$match.$and.push({measurement_template: {$in: measurementTemplates.map(e => e._id)}}); // find measurements to sort + // find measurements to sort + queryPtr[0].$match.$and.push({measurement_template: {$in: measurementTemplates.map(e => e._id)}}); if (filters.filters.find(e => e.field === filters.sort[0])) { // sorted measurement should also be filtered - queryPtr[0].$match.$and.push(...filterQueries(filters.filters.filter(e => e.field === filters.sort[0]).map(e => {e.field = 'values.' + e.field.split('.')[2]; return e; }))); + queryPtr[0].$match.$and.push(...filterQueries(filters.filters.filter(e => e.field === filters.sort[0]) + .map(e => {e.field = 'values.' + e.field.split('.')[2]; return e; }))); } queryPtr.push( ...sortQuery(filters, ['values.' + measurementParam, 'sample_id'], sortStartValue), // sort measurements - {$replaceRoot: {newRoot: {measurement: '$$ROOT'}}}, // fetch samples and restructure them to fit sample structure + {$replaceRoot: {newRoot: {measurement: '$$ROOT'}}}, // fetch samples and restructure them to fit sample structure {$lookup: {from: 'samples', localField: 'measurement.sample_id', foreignField: '_id', as: 'sample'}}, {$match: statusQuery(filters, 'sample.status')}, // filter out wrong status once samples were added {$addFields: {['sample.' + measurementName]: '$measurement.values'}}, // more restructuring @@ -159,43 +170,52 @@ router.get('/samples', async (req, res, next) => { let materialAdded = false; if (sortFilterKeys.find(e => /material\./.test(e))) { // add material fields materialAdded = true; - materialQuery.push( // add material properties - {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, // TODO: project out unnecessary fields + materialQuery.push( // add material properties // TODO: project out unnecessary fields + {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, {$addFields: {material: {$arrayElemAt: ['$material', 0]}}} ); - const baseMFilters = sortFilterKeys.filter(e => /material\./.test(e)).filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) < 0); - addFilterQueries(materialQuery, filters.filters.filter(e => baseMFilters.indexOf(e.field) >= 0)); // base material filters + const baseMFilters = sortFilterKeys.filter(e => /material\./.test(e)) + .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) < 0); + // base material filters + addFilterQueries(materialQuery, filters.filters.filter(e => baseMFilters.indexOf(e.field) >= 0)); if (sortFilterKeys.find(e => e === 'material.supplier')) { // add supplier if needed materialQuery.push( - {$lookup: { from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'}}, + {$lookup: { + from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'} + }, {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}} ); } if (sortFilterKeys.find(e => e === 'material.group')) { // add group if needed materialQuery.push( - {$lookup: { from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' }}, + {$lookup: { + from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' } + }, {$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}} ); } if (sortFilterKeys.find(e => e === 'material.number')) { // add material number if needed materialQuery.push( - {$addFields: {'material.number': { $arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}]}}} + {$addFields: {'material.number': { $arrayElemAt: [ + '$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']} + ]}}} ); } - const specialMFilters = sortFilterKeys.filter(e => /material\./.test(e)).filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) >= 0); - addFilterQueries(materialQuery, filters.filters.filter(e => specialMFilters.indexOf(e.field) >= 0)); // base material filters + const specialMFilters = sortFilterKeys.filter(e => /material\./.test(e)) + .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) >= 0); + // base material filters + addFilterQueries(materialQuery, filters.filters.filter(e => specialMFilters.indexOf(e.field) >= 0)); queryPtr.push(...materialQuery); if (/material\./.test(filters.sort[0])) { // sort by material key let sortStartValue = null; if (filters['from-id']) { // from-id specified - const fromSample = await SampleModel.aggregate([{$match: {_id: mongoose.Types.ObjectId(filters['from-id'])}}, ...materialQuery]).exec().catch(err => {next(err);}); + const fromSample = await SampleModel.aggregate( + [{$match: {_id: mongoose.Types.ObjectId(filters['from-id'])}}, ...materialQuery] + ).exec().catch(err => {next(err);}); if (fromSample instanceof Error) return; if (!fromSample) { return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'}); } - console.log(fromSample); - console.log(filters.sort[0]); - console.log(fromSample[filters.sort[0]]); const filterKey = filters.sort[0].split('.'); if (filterKey.length === 2) { sortStartValue = fromSample[0][filterKey[0]][filterKey[1]]; @@ -208,23 +228,25 @@ router.get('/samples', async (req, res, next) => { } } - const measurementFilterFields = _.uniq(sortFilterKeys.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1])); // filter measurement names and remove duplicates from parameters + const measurementFilterFields = _.uniq(sortFilterKeys.filter(e => /measurements\./.test(e)) + .map(e => e.split('.')[1])); // filter measurement names and remove duplicates from parameters if (sortFilterKeys.find(e => /measurements\./.test(e))) { // add measurement fields - const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFilterFields}}).lean().exec().catch(err => {next(err);}); + const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFilterFields}}) + .lean().exec().catch(err => {next(err);}); if (measurementTemplates instanceof Error) return; if (measurementTemplates.length < measurementFilterFields.length) { return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'}); } queryPtr.push({$lookup: { from: 'measurements', let: {sId: '$_id'}, - pipeline: [{$match: {$expr: {$and: [{$eq: ['$sample_id', '$$sId']}, {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]}]}}}], + pipeline: [{$match: {$expr: {$and: [ + {$eq: ['$sample_id', '$$sId']}, + {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]} + ]}}}], as: 'measurements' }}); measurementTemplates.forEach(template => { - queryPtr.push({$addFields: {[template.name]: {$let: { // add measurements as property [template.name], if one result, array is reduced to direct values - vars: {arr: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', mongoose.Types.ObjectId(template._id)]}}}}, - in:{$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']} - }}}}, {$addFields: {[template.name]: {$cond: ['$' + template.name + '.values', '$' + template.name + '.values', template.parameters.reduce((s, e) => {s[e.name] = null; return s;}, {})]}}}); + addMeasurements(queryPtr, template); }); addFilterQueries(queryPtr, filters.filters .filter(e => sortFilterKeys.filter(e => /measurements\./.test(e)).indexOf(e.field) >= 0) @@ -232,14 +254,18 @@ router.get('/samples', async (req, res, next) => { ); // measurement filters } - if (!filters.fields.find(e => /spectrum\./.test(e)) && !filters['from-id']) { // count total number of items before $skip and $limit, only works when from-id is not specified and spectra are not included + // count total number of items before $skip and $limit, only works when from-id is not specified and spectra are not + // included + if (!filters.fields.find(e => /spectrum\./.test(e)) && !filters['from-id']) { queryPtr.push({$facet: {count: [{$count: 'count'}], samples: []}}); queryPtr = queryPtr[queryPtr.length - 1].$facet.samples; // add rest of aggregation pipeline into $facet } // paging if (filters['to-page']) { - queryPtr.push({$skip: Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] + Number(filters['to-page'] < 0)}) // number to skip, if going back pages, one page has to be skipped less but on sample more + // number to skip, if going back pages, one page has to be skipped less but on sample more + queryPtr.push({$skip: Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] + + Number(filters['to-page'] < 0)}) } if (filters['page-size']) { queryPtr.push({$limit: filters['page-size']}); @@ -265,51 +291,65 @@ router.get('/samples', async (req, res, next) => { } if (fieldsToAdd.indexOf('material.supplier') >= 0) { // add supplier if needed queryPtr.push( - {$lookup: { from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'}}, + {$lookup: { + from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier' + }}, {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}} ); } if (fieldsToAdd.indexOf('material.group') >= 0) { // add group if needed queryPtr.push( - {$lookup: { from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' }}, + {$lookup: { + from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' + }}, {$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}} ); } if (fieldsToAdd.indexOf('material.number') >= 0) { // add material number if needed queryPtr.push( - {$addFields: {'material.number': { $arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}]}}} + {$addFields: {'material.number': { + $arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}] + }}} ); } - let measurementFieldsFields: string[] = _.uniq(fieldsToAdd.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1])); // filter measurement names and remove duplicates from parameters + let measurementFieldsFields: string[] = _.uniq( + fieldsToAdd.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1]) + ); // filter measurement names and remove duplicates from parameters if (fieldsToAdd.find(e => /measurements\./.test(e))) { // add measurement fields - const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFieldsFields}}).lean().exec().catch(err => {next(err);}); + const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFieldsFields}}) + .lean().exec().catch(err => {next(err);}); if (measurementTemplates instanceof Error) return; if (measurementTemplates.length < measurementFieldsFields.length) { return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'}); } - if (fieldsToAdd.find(e => /spectrum\./.test(e))) { // use different lookup methods with and without spectrum for the best performance - queryPtr.push({$lookup: {from: 'measurements', localField: '_id', foreignField: 'sample_id', as: 'measurements'}}); + // use different lookup methods with and without spectrum for the best performance + if (fieldsToAdd.find(e => /spectrum\./.test(e))) { + queryPtr.push( + {$lookup: {from: 'measurements', localField: '_id', foreignField: 'sample_id', as: 'measurements'}} + ); } else { queryPtr.push({$lookup: { from: 'measurements', let: {sId: '$_id'}, - pipeline: [{$match: {$expr: {$and: [{$eq: ['$sample_id', '$$sId']}, {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]}]}}}], + pipeline: [{$match: {$expr: {$and: [ + {$eq: ['$sample_id', '$$sId']}, + {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]} + ]}}}], as: 'measurements' }}); } measurementTemplates.forEach(template => { // TODO: hard coded dpt for special treatment, change later - queryPtr.push({$addFields: {[template.name]: {$let: { // add measurements as property [template.name], if one result, array is reduced to direct values - vars: {arr: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', mongoose.Types.ObjectId(template._id)]}}}}, - in:{$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']} - }}}}, {$addFields: {[template.name]: {$cond: ['$' + template.name + '.values', '$' + template.name + '.values', template.parameters.reduce((s, e) => {s[e.name] = null; return s;}, {})]}}}); + addMeasurements(queryPtr, template); if (measurementFieldsFields.find(e => e === 'spectrum')) { queryPtr.push({$unwind: '$spectrum'}); } }); // if (measurementFieldsFields.find(e => e === 'spectrum')) { // TODO: remove hardcoded as well // queryPtr.push( - // {$addFields: {spectrum: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', measurementTemplates.filter(e => e.name === 'spectrum')[0]._id]}}}}}, + // {$addFields: {spectrum: {$filter: {input: '$measurements', cond: { + // $eq: ['$$this.measurement_template', measurementTemplates.filter(e => e.name === 'spectrum')[0]._id] + // }}}}}, // {$addFields: {spectrum: '$spectrum.values'}}, // {$unwind: '$spectrum'} // ); @@ -318,10 +358,11 @@ router.get('/samples', async (req, res, next) => { queryPtr.push({$project: {measurements: 0}}); } - const projection = filters.fields.map(e => e.replace('measurements.', '')).reduce((s, e) => {s[e] = true; return s; }, {}); - if (filters.fields.indexOf('added') >= 0) { // add added date + const projection = filters.fields.map(e => e.replace('measurements.', '')) + .reduce((s, e) => {s[e] = true; return s; }, {}); + if (filters.fields.indexOf('added') >= 0) { // add added date // TODO: upgrade MongoDB version or find alternative // projection.added = {$toDate: '$_id'}; - // projection.added = { $convert: { input: '$_id', to: "date" } } // TODO: upgrade MongoDB version or find alternative + // projection.added = { $convert: { input: '$_id', to: "date" } } } if (filters.fields.indexOf('_id') < 0 && filters.fields.indexOf('added') < 0) { // disable _id explicitly projection._id = false; @@ -347,7 +388,10 @@ router.get('/samples', async (req, res, next) => { if (filters['to-page'] < 0) { data.reverse(); } - const measurementFields = _.uniq([filters.sort[0].split('.')[1], ...measurementFilterFields, ...measurementFieldsFields]); + const measurementFields = _.uniq( + [filters.sort[0].split('.')[1], + ...measurementFilterFields, ...measurementFieldsFields] + ); if (filters.csv) { // output as csv csv(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))), (err, data) => { if (err) return next(err); @@ -355,8 +399,8 @@ router.get('/samples', async (req, res, next) => { res.send(data); }); } - else { - res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields)))); // validate all and filter null values from validation errors + else { // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields)))); } }); } @@ -385,16 +429,17 @@ router.get('/samples', async (req, res, next) => { }); router.get('/samples/:state(new|deleted)', (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; SampleModel.find({status: globals.status[req.params.state]}).lean().exec((err, data) => { if (err) return next(err); - res.json(_.compact(data.map(e => SampleValidate.output(e)))); // validate all and filter null values from validation errors + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => SampleValidate.output(e)))); }); }); router.get('/samples/count', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; SampleModel.estimatedDocumentCount((err, data) => { if (err) return next(err); @@ -403,18 +448,20 @@ router.get('/samples/count', (req, res, next) => { }); router.get('/sample/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id').exec(async (err, sampleData: any) => { - if (err) return next(err); + SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id') + .exec(async (err, sampleData: any) => { + if (err) return next(err); await sampleReturn(sampleData, req, res, next); }); }); router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; const {error, value: sample} = SampleValidate.input(req.body, 'change'); + console.log(error); if (error) return res400(error, res); SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists @@ -426,16 +473,19 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { return res.status(403).json({status: 'Forbidden'}); } - // only maintain and admin are allowed to edit other user's data - if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return; + // only dev and admin are allowed to edit other user's data + if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return; if (sample.hasOwnProperty('material_id')) { if (!await materialCheck(sample, res, next)) return; } else if (sample.hasOwnProperty('color')) { if (!await materialCheck(sample, res, next, sampleData.material_id)) return; } - if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) { // do not execute check if condition is and was empty - if (!await conditionCheck(sample.condition, 'change', res, next, sampleData.condition.condition_template.toString() !== sample.condition.condition_template)) return; + // do not execute check if condition is and was empty + if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) { + if (!await conditionCheck(sample.condition, 'change', res, next, + !(sampleData.condition.condition_template && + sampleData.condition.condition_template.toString() === sample.condition.condition_template))) return; } if (sample.hasOwnProperty('notes')) { @@ -443,7 +493,8 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { if (sampleData.note_id !== null) { // old notes data exists const data = await NoteModel.findById(sampleData.note_id).lean().exec().catch(err => {next(err);}) as any; if (data instanceof Error) return; - newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes); // check if notes were changed + // check if notes were changed + newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes); if (newNotes) { if (data.hasOwnProperty('custom_fields')) { // update note_fields customFieldsChange(Object.keys(data.custom_fields), -1, req); @@ -456,7 +507,8 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { if (_.keys(sample.notes).length > 0 && newNotes) { // save new notes if (!await sampleRefCheck(sample, res, next)) return; - if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { // new custom_fields + // new custom_fields + if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req); } let data = await new NoteModel(sample.notes).save().catch(err => { return next(err)}); // save new notes @@ -480,7 +532,7 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { }); router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists if (err) return next(err); @@ -488,14 +540,16 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => { return res.status(404).json({status: 'Not found'}); } - // only maintain and admin are allowed to edit other user's data - if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return; + // only dev and admin are allowed to edit other user's data + if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return; - await SampleModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).lean().exec(err => { // set sample status + // set sample status + await SampleModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).lean().exec(err => { if (err) return next(err); // set status of associated measurements also to deleted - MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status: -1}).log(req).lean().exec(err => { + MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status: -1}) + .log(req).lean().exec(err => { if (err) return next(err); if (sampleData.note_id !== null) { // handle notes @@ -516,16 +570,17 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => { }); router.get('/sample/number/:number', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - SampleModel.findOne({number: req.params.number}).populate('material_id').populate('user_id', 'name').populate('note_id').exec(async (err, sampleData: any) => { + SampleModel.findOne({number: req.params.number}).populate('material_id').populate('user_id', 'name') + .populate('note_id').exec(async (err, sampleData: any) => { if (err) return next(err); await sampleReturn(sampleData, req, res, next); }); }); router.put('/sample/restore/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; SampleModel.findByIdAndUpdate(req.params.id, {status: globals.status.new}).log(req).lean().exec((err, data) => { if (err) return next(err); @@ -538,47 +593,34 @@ router.put('/sample/restore/' + IdValidate.parameter(), (req, res, next) => { }); router.put('/sample/validate/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - SampleModel.findById(req.params.id).lean().exec((err, data: any) => { + SampleModel.findByIdAndUpdate(req.params.id, {status: globals.status.validated}).log(req).lean().exec((err, data) => { if (err) return next(err); - if (!data) { return res.status(404).json({status: 'Not found'}); } - if (Object.keys(data.condition).length === 0) { - return res.status(400).json({status: 'Sample without condition cannot be valid'}); - } - MeasurementModel.find({sample_id: mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => { - if (err) return next(err); - - if (data.length === 0) { - return res.status(400).json({status: 'Sample without measurements cannot be valid'}); - } - - SampleModel.findByIdAndUpdate(req.params.id, {status: globals.status.validated}).log(req).lean().exec(err => { - if (err) return next(err); - res.json({status: 'OK'}); - }); - }); + res.json({status: 'OK'}); }); }); router.post('/sample/new', async (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; if (!req.body.hasOwnProperty('condition')) { // add empty condition if not specified req.body.condition = {}; } - const {error, value: sample} = SampleValidate.input(req.body, 'new' + (req.authDetails.level === 'admin' ? '-admin' : '')); + const {error, value: sample} = + SampleValidate.input(req.body, 'new' + (req.authDetails.level === 'admin' ? '-admin' : '')); if (error) return res400(error, res); if (!await materialCheck(sample, res, next)) return; if (!await sampleRefCheck(sample, res, next)) return; - if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { // new custom_fields + // new custom_fields + if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req); } @@ -611,25 +653,31 @@ router.post('/sample/new', async (req, res, next) => { }); router.get('/sample/notes/fields', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; NoteFieldModel.find({}).lean().exec((err, data) => { if (err) return next(err); - res.json(_.compact(data.map(e => NoteFieldValidate.output(e)))); // validate all and filter null values from validation errors + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => NoteFieldValidate.output(e)))); }) }); module.exports = router; +// store the highest generated number for each location to avoid duplicate numbers +const numberBuffer: {[location: string]: number} = {}; -async function numberGenerate (sample, req, res, next) { // generate number in format Location32, returns false on error +// generate number in format Location32, returns false on error +async function numberGenerate (sample, req, res, next) { const sampleData = await SampleModel .aggregate([ {$match: {number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')}}, - // {$addFields: {number2: {$toDecimal: {$arrayElemAt: [{$split: [{$arrayElemAt: [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}}}}, // not working with MongoDb 3.6 + // {$addFields: {number2: {$toDecimal: {$arrayElemAt: [{$split: [{$arrayElemAt: + // [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}}}}, // not working with MongoDb 3.6 {$addFields: {sortNumber: {$let: { - vars: {tmp: {$concat: ['000000000000000000000000000000', {$arrayElemAt: [{$split: [{$arrayElemAt: [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}]}}, + vars: {tmp: {$concat: ['000000000000000000000000000000', + {$arrayElemAt: [{$split: [{$arrayElemAt: [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}]}}, in: {$substrCP: ['$$tmp', {$subtract: [{$strLenCP: '$$tmp'}, 30]}, {$strLenCP: '$$tmp'}]} }}}}, {$sort: {sortNumber: -1}}, @@ -638,11 +686,18 @@ async function numberGenerate (sample, req, res, next) { // generate number in .exec() .catch(err => next(err)); if (sampleData instanceof Error) return false; - return req.authDetails.location + (sampleData[0] ? Number(sampleData[0].number.replace(/[^0-9]+/g, '')) + 1 : 1); + let number = (sampleData[0] ? Number(sampleData[0].number.replace(/[^0-9]+/g, '')) : 0); + if (numberBuffer[req.authDetails.location] && numberBuffer[req.authDetails.location] >= number) { + number = numberBuffer[req.authDetails.location]; + } + number ++; + numberBuffer[req.authDetails.location] = number; + return req.authDetails.location + number; } async function numberCheck(sample, res, next) { - const sampleData = await SampleModel.findOne({number: sample.number}).lean().exec().catch(err => {next(err); return false;}); + const sampleData = await SampleModel.findOne({number: sample.number}) + .lean().exec().catch(err => {next(err); return false;}); if (sampleData) { // found entry with sample number res.status(400).json({status: 'Sample number already taken'}); return false @@ -650,7 +705,8 @@ async function numberCheck(sample, res, next) { return true; } -async function materialCheck (sample, res, next, id = sample.material_id) { // validate material_id and color, returns false if invalid +// validate material_id and color, returns false if invalid +async function materialCheck (sample, res, next, id = sample.material_id) { const materialData = await MaterialModel.findById(id).lean().exec().catch(err => next(err)) as any; if (materialData instanceof Error) return false; if (!materialData) { // could not find material_id @@ -660,12 +716,14 @@ async function materialCheck (sample, res, next, id = sample.material_id) { // return true; } -async function conditionCheck (condition, param, res, next, checkVersion = true) { // validate treatment template, returns false if invalid, otherwise template data +// validate treatment template, returns false if invalid, otherwise template data +async function conditionCheck (condition, param, res, next, checkVersion = true) { if (!condition.condition_template || !IdValidate.valid(condition.condition_template)) { // template id not found res.status(400).json({status: 'Condition template not available'}); return false; } - const conditionData = await ConditionTemplateModel.findById(condition.condition_template).lean().exec().catch(err => next(err)) as any; + const conditionData = await ConditionTemplateModel.findById(condition.condition_template) + .lean().exec().catch(err => next(err)) as any; if (conditionData instanceof Error) return false; if (!conditionData) { // template not found res.status(400).json({status: 'Condition template not available'}); @@ -674,7 +732,8 @@ async function conditionCheck (condition, param, res, next, checkVersion = true) if (checkVersion) { // get all template versions and check if given is latest - const conditionVersions = await ConditionTemplateModel.find({first_id: conditionData.first_id}).sort({version: -1}).lean().exec().catch(err => next(err)) as any; + const conditionVersions = await ConditionTemplateModel.find({first_id: conditionData.first_id}) + .sort({version: -1}).lean().exec().catch(err => next(err)) as any; if (conditionVersions instanceof Error) return false; if (condition.condition_template !== conditionVersions[0]._id.toString()) { // template not latest res.status(400).json({status: 'Old template version not allowed'}); @@ -683,14 +742,16 @@ async function conditionCheck (condition, param, res, next, checkVersion = true) } // validate parameters - const {error, value: ignore} = ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param); + const {error, value: ignore} = + ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param); if (error) {res400(error, res); return false;} return conditionData; } function sampleRefCheck (sample, res, next) { // validate sample_references, resolves false for invalid reference return new Promise(resolve => { - if (sample.notes.hasOwnProperty('sample_references') && sample.notes.sample_references.length > 0) { // there are sample_references + // there are sample_references + if (sample.notes.hasOwnProperty('sample_references') && sample.notes.sample_references.length > 0) { let referencesCount = sample.notes.sample_references.length; // count to keep track of running async operations sample.notes.sample_references.forEach(reference => { @@ -715,7 +776,8 @@ function sampleRefCheck (sample, res, next) { // validate sample_references, re function customFieldsChange (fields, amount, req) { // update custom_fields and respective quantities fields.forEach(field => { - NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}} as any, {new: true}).log(req).lean().exec((err, data: any) => { // check if field exists + NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}} as any, {new: true}) + .log(req).lean().exec((err, data: any) => { // check if field exists if (err) return console.error(err); if (!data) { // new field new NoteFieldModel({name: field, qty: 1}).save((err, data) => { @@ -735,11 +797,27 @@ function customFieldsChange (fields, amount, req) { // update custom_fields and function sortQuery(filters, sortKeys, sortStartValue) { // sortKeys = ['primary key', 'secondary key'] if (filters['from-id']) { // from-id specified if ((filters['to-page'] === 0 && filters.sort[1] === 1) || (filters.sort[1] * filters['to-page'] > 0)) { // asc - return [{$match: {$or: [{[sortKeys[0]]: {$gt: sortStartValue}}, {$and: [{[sortKeys[0]]: sortStartValue}, {[sortKeys[1]]: {$gte: new mongoose.Types.ObjectId(filters['from-id'])}}]}]}}, - {$sort: {[sortKeys[0]]: 1, _id: 1}}]; + return [ + {$match: {$or: [ + {[sortKeys[0]]: {$gt: sortStartValue}}, + {$and: [ + {[sortKeys[0]]: sortStartValue}, + {[sortKeys[1]]: {$gte: new mongoose.Types.ObjectId(filters['from-id'])}} + ]} + ]}}, + {$sort: {[sortKeys[0]]: 1, _id: 1}} + ]; } else { - return [{$match: {$or: [{[sortKeys[0]]: {$lt: sortStartValue}}, {$and: [{[sortKeys[0]]: sortStartValue}, {[sortKeys[1]]: {$lte: new mongoose.Types.ObjectId(filters['from-id'])}}]}]}}, - {$sort: {[sortKeys[0]]: -1, _id: -1}}]; + return [ + {$match: {$or: [ + {[sortKeys[0]]: {$lt: sortStartValue}}, + {$and: [ + {[sortKeys[0]]: sortStartValue}, + {[sortKeys[1]]: {$lte: new mongoose.Types.ObjectId(filters['from-id'])}} + ]} + ]}}, + {$sort: {[sortKeys[0]]: -1, _id: -1}} + ]; } } else { // sort from beginning return [{$sort: {[sortKeys[0]]: filters.sort[1], [sortKeys[1]]: filters.sort[1]}}]; // set _id as secondary sort @@ -775,30 +853,57 @@ function filterQueries (filters) { return {[e.field]: {['$in']: [new RegExp(e.values[0])]}}; } else { - return {[e.field]: {['$' + e.mode]: (e.mode.indexOf('in') >= 0 ? e.values : e.values[0])}}; // add filter criteria as {field: {$mode: value}}, only use first value when mode is not in/nin + // add filter criteria as {field: {$mode: value}}, only use first value when mode is not in/nin + return {[e.field]: {['$' + e.mode]: (e.mode.indexOf('in') >= 0 ? e.values : e.values[0])}}; } }); } +// add measurements as property [template.name], if one result, array is reduced to direct values +function addMeasurements(queryPtr, template) { + queryPtr.push( + {$addFields: {[template.name]: {$let: {vars: { + arr: {$filter: { + input: '$measurements', cond: {$eq: ['$$this.measurement_template', mongoose.Types.ObjectId(template._id)]} + }}}, + in: {$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']} + }}}}, + {$addFields: {[template.name]: {$cond: [ + '$' + template.name + '.values', + '$' + template.name + '.values', + template.parameters.reduce((s, e) => {s[e.name] = null; return s;}, {}) + ]}}} + ); +} + function dateToOId (date) { // convert date to ObjectId return mongoose.Types.ObjectId(Math.floor(date / 1000).toString(16) + '0000000000000000'); } async function sampleReturn (sampleData, req, res, next) { if (sampleData) { - console.log(sampleData); - await sampleData.populate('material_id.group_id').populate('material_id.supplier_id').execPopulate().catch(err => next(err)); + await sampleData.populate('material_id.group_id').populate('material_id.supplier_id') + .execPopulate().catch(err => next(err)); if (sampleData instanceof Error) return; sampleData = sampleData.toObject(); - if (sampleData.status === globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted samples only available for maintain/admin + // deleted samples only available for dev/admin + if (sampleData.status === globals.status.deleted && !req.auth(res, ['dev', 'admin'], 'all')) return; sampleData.material = sampleData.material_id; // map data to right keys sampleData.material.group = sampleData.material.group_id.name; sampleData.material.supplier = sampleData.material.supplier_id.name; sampleData.user = sampleData.user_id.name; sampleData.notes = sampleData.note_id ? sampleData.note_id : {}; - MeasurementModel.find({sample_id: sampleData._id, status: {$ne: globals.status.deleted}}).lean().exec((err, data) => { + MeasurementModel.find({sample_id: sampleData._id, status: {$ne: globals.status.deleted}}) + .lean().exec((err, data) => { sampleData.measurements = data; + if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) { // strip dpt values if not dev or admin + sampleData.measurements.forEach(measurement => { + if (measurement.values.dpt) { + delete measurement.values.dpt; + } + }); + } res.json(SampleValidate.output(sampleData, 'details')); }); } diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts index db924b3..f936c46 100644 --- a/src/routes/template.spec.ts +++ b/src/routes/template.spec.ts @@ -4,6 +4,7 @@ import TemplateConditionModel from '../models/condition_template'; import TemplateMeasurementModel from '../models/measurement_template'; import TestHelper from "../test/helper"; +// TODO: method to return only latest template versions -> rework frontend accordingly describe('/template', () => { let server; diff --git a/src/routes/template.ts b/src/routes/template.ts index 5641d1b..ad73a8c 100644 --- a/src/routes/template.ts +++ b/src/routes/template.ts @@ -15,17 +15,18 @@ import db from '../db'; const router = express.Router(); router.get('/template/:collection(measurements|conditions|materials)', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; req.params.collection = req.params.collection.replace(/s$/g, ''); // remove trailing s model(req).find({}).lean().exec((err, data) => { if (err) next (err); - res.json(_.compact(data.map(e => TemplateValidate.output(e)))); // validate all and filter null values from validation errors + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => TemplateValidate.output(e)))); }); }); router.get('/template/:collection(measurement|condition|material)/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; model(req).findById(req.params.id).lean().exec((err, data) => { if (err) next (err); @@ -38,8 +39,9 @@ router.get('/template/:collection(measurement|condition|material)/' + IdValidate }); }); -router.put('/template/:collection(measurement|condition|material)/' + IdValidate.parameter(), async (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; +router.put('/template/:collection(measurement|condition|material)/' + IdValidate.parameter(), + async (req, res, next) => { + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; const {error, value: template} = TemplateValidate.input(req.body, 'change'); if (error) return res400(error, res); @@ -51,7 +53,8 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate return res.status(404).json({status: 'Not found'}); } // find latest version - const templateData = await model(req).findOne({first_id: templateRef.first_id}).sort({version: -1}).lean().exec().catch(err => {next(err);}) as any; + const templateData = await model(req).findOne({first_id: templateRef.first_id}).sort({version: -1}) + .lean().exec().catch(err => {next(err);}) as any; if (templateData instanceof Error) return; if (!templateData) { return res.status(404).json({status: 'Not found'}); @@ -59,7 +62,8 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed template.version = templateData.version + 1; // increase version - await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => { // save new template, fill with old properties + // save new template, fill with old properties + await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => { if (err) next (err); db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject()); res.json(TemplateValidate.output(data.toObject())); @@ -71,7 +75,7 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate }); router.post('/template/:collection(measurement|condition|material)/new', async (req, res, next) => { - if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; const {error, value: template} = TemplateValidate.input(req.body, 'new'); if (error) return res400(error, res); diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index a39bc50..cbeda33 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -564,7 +564,7 @@ describe('/user', () => { 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', details: '"level" must be one of [read, write, maintain, dev, admin]'} + res: {status: 'Invalid body format', details: '"level" must be one of [read, write, dev, admin]'} }); }); it('rejects an invalid email address', done => { diff --git a/src/routes/user.ts b/src/routes/user.ts index 2393150..7b43449 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -16,12 +16,15 @@ router.get('/users', (req, res) => { if (!req.auth(res, ['admin'], 'basic')) return; UserModel.find({}).lean().exec( (err, data:any) => { - res.json(_.compact(data.map(e => UserValidate.output(e)))); // validate all and filter null values from validation errors + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => UserValidate.output(e)))); }); }); -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 - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; +// 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 +router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; const username = getUsername(req, res); if (!username) return; @@ -36,13 +39,15 @@ router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi }); }); -router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; +// this path matches /user, /user/ and /user/xxx, but not /user/key or user/new +router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; const username = getUsername(req, res); if (!username) return; - const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : '')); + const {error, value: user} = UserValidate.input(req.body, 'change' + + (req.authDetails.level === 'admin'? 'admin' : '')); if (error) return res400(error, res); if (user.hasOwnProperty('pass')) { @@ -66,8 +71,10 @@ router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => { }); // TODO: only possible if no data is linked to user, otherwise change status, etc. -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 - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; +// 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 +router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; const username = getUsername(req, res); if (!username) return; @@ -84,7 +91,7 @@ router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // }); router.get('/user/key', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; UserModel.findOne({name: req.authDetails.username}).lean().exec( (err, data:any) => { if (err) return next(err); @@ -126,7 +133,10 @@ router.post('/user/passreset', (req, res, next) => { if (err) return next(err); // send email - 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 => { + mail(data[0].email, 'Your new password for the DeFinMa 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 DeFinMa team', err => { if (err) return next(err); res.json({status: 'OK'}); }); diff --git a/src/routes/validate/measurement.ts b/src/routes/validate/measurement.ts index 0af8fbd..f7d8c70 100644 --- a/src/routes/validate/measurement.ts +++ b/src/routes/validate/measurement.ts @@ -34,8 +34,12 @@ export default class MeasurementValidate { } } - static output (data) { // validate output and strip unwanted properties, returns null if not valid + static output (data, req) { // validate output and strip unwanted properties, returns null if not valid data = IdValidate.stringify(data); + // spectral data not allowed for read/write users + if (['dev', 'admin'].indexOf(req.authDetails.level) < 0 && data.values.dpt) { + delete data.values.dpt; + } const {value, error} = Joi.object({ _id: IdValidate.get(), sample_id: IdValidate.get(), diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts index 19f6b50..d92674d 100644 --- a/src/routes/validate/sample.ts +++ b/src/routes/validate/sample.ts @@ -15,7 +15,7 @@ export default class SampleValidate { .allow(''), type: Joi.string() - .max(128), + .valid('granulate', 'part', 'tension rod'), batch: Joi.string() .max(128) @@ -116,7 +116,8 @@ export default class SampleValidate { } } - static output (data, param = 'refs+added', additionalParams = []) { // validate output and strip unwanted properties, returns null if not valid + // validate output and strip unwanted properties, returns null if not valid + static output (data, param = 'refs+added', additionalParams = []) { if (param === 'refs+added') { param = 'refs'; data.added = data._id.getTimestamp(); @@ -169,12 +170,16 @@ export default class SampleValidate { if (filterValidation.error) return filterValidation; try { for (let i in data.filters) { + // data.filters[i] = JSON.parse(decodeURIComponent(data.filters[i])); data.filters[i] = JSON.parse(data.filters[i]); data.filters[i].values = data.filters[i].values.map(e => { // validate filter values let validator; let field = data.filters[i].field if (/material\./.test(field)) { // select right validation model - validator = MaterialValidate.outputV().append({number: Joi.string().max(128).allow(''), properties: Joi.alternatives().try(Joi.number(), Joi.string().max(128))}); + validator = MaterialValidate.outputV().append({ + number: Joi.string().max(128).allow(''), + properties: Joi.alternatives().try(Joi.number(), Joi.string().max(128)) + }); field = field.replace('material.', '').split('.')[0]; } else if (/measurements\./.test(field)) { @@ -194,12 +199,12 @@ export default class SampleValidate { validator = Joi.object(this.sample); } const {value, error} = validator.validate({[field]: e}); - if (error) throw error; // reject invalid values // TODO: return exact error description, handle in frontend filters + if (error) throw error; // reject invalid values return value[field]; }); } } - catch { + catch (err) { return {error: {details: [{message: 'Invalid JSON string for filter parameter'}]}, value: null} } } @@ -208,13 +213,22 @@ export default class SampleValidate { 'from-id': IdValidate.get(), 'to-page': Joi.number().integer(), 'page-size': Joi.number().integer().min(1), - sort: Joi.string().pattern(new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')-(asc|desc)$', 'm')).default('_id-asc'), + sort: Joi.string().pattern( + new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')-(asc|desc)$', 'm') + ).default('_id-asc'), csv: Joi.boolean().default(false), - fields: Joi.array().items(Joi.string().pattern(new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm'))).default(['_id','number','type','batch','material_id','color','condition','note_id','user_id','added']).messages({'string.pattern.base': 'Invalid field name'}), + fields: Joi.array().items(Joi.string().pattern( + new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm') + )).default(['_id','number','type','batch','material_id','color','condition','note_id','user_id','added']) + .messages({'string.pattern.base': 'Invalid field name'}), filters: Joi.array().items(Joi.object({ mode: Joi.string().valid('eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'in', 'nin', 'stringin'), - field: Joi.string().pattern(new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm')).messages({'string.pattern.base': 'Invalid filter field name'}), - values: Joi.array().items(Joi.alternatives().try(Joi.string().max(128), Joi.number(), Joi.boolean(), Joi.date().iso(), Joi.object())).min(1) + field: Joi.string().pattern( + new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm') + ).messages({'string.pattern.base': 'Invalid filter field name'}), + values: Joi.array().items(Joi.alternatives().try( + Joi.string().max(128), Joi.number(), Joi.boolean(), Joi.date().iso(), Joi.object() + )).min(1) })).default([]) }).with('to-page', 'page-size').validate(data); } diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts index aed8f68..4616a70 100644 --- a/src/routes/validate/template.ts +++ b/src/routes/validate/template.ts @@ -1,7 +1,7 @@ import Joi from '@hapi/joi'; import IdValidate from './id'; -// TODO: do not allow a . in the name +// TODO: do not allow a . in the name !!! export default class TemplateValidate { private static template = { name: Joi.string() diff --git a/src/test/db.json b/src/test/db.json index 7930a94..f95ed05 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -99,7 +99,7 @@ { "_id": {"$oid":"400000000000000000000007"}, "number": "34", - "type": "liquid", + "type": "part", "color": "black", "batch": "", "condition": {}, diff --git a/src/test/helper.ts b/src/test/helper.ts index 44085f7..6fe16d8 100644 --- a/src/test/helper.ts +++ b/src/test/helper.ts @@ -29,7 +29,10 @@ export default class TestHelper { } static beforeEach (server, done) { - delete require.cache[require.resolve('../index')]; // prevent loading from cache + // delete cached server code except models as these are needed in the testing files as well + Object.keys(require.cache).filter(e => /API\\dist\\(?!(models|db|test))/.test(e)).forEach(key => { + delete require.cache[key]; // prevent loading from cache + }); server = require('../index'); db.drop(err => { // reset database if (err) return done(err); @@ -38,10 +41,13 @@ export default class TestHelper { return server } - static request (server, done, options) { // options in form: {method, url, contentType, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res, default (set to false if you want to dismiss default .end handling)} + // options in form: {method, url, contentType, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res, + // default (set to false if you want to dismiss default .end handling)} + static request (server, done, options) { let st = supertest(server); if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) { // resolve API key - options.url += '?key=' + (this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key); + options.url += '?key=' + + (this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key); } switch (options.method) { // http method case 'get': @@ -91,10 +97,12 @@ export default class TestHelper { done(); }); } - else if (options.hasOwnProperty('log')) { // check changelog, takes log: {collection, skip, data/(dataAdd, dataIgn)} + // check changelog, takes log: {collection, skip, data/(dataAdd, dataIgn)} + else if (options.hasOwnProperty('log')) { return st.end(err => { if (err) return done (err); - ChangelogModel.findOne({}).sort({_id: -1}).skip(options.log.skip? options.log.skip : 0).lean().exec((err, data) => { // latest entry + ChangelogModel.findOne({}).sort({_id: -1}).skip(options.log.skip? options.log.skip : 0) + .lean().exec((err, data) => { // latest entry if (err) return done(err); should(data).have.only.keys('_id', 'action', 'collectionName', 'conditions', 'data', 'user_id', '__v'); should(data).have.property('action', options.method.toUpperCase() + ' ' + options.url);