diff --git a/.gitignore b/.gitignore index 0a811ca..645d3a5 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,4 @@ dist **/.idea/tasks.xml **/.idea/shelf **/.idea/*.iml +/tmp/ diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/dictionaries/VLE2FE.xml b/.idea/dictionaries/VLE2FE.xml new file mode 100644 index 0000000..c274b8b --- /dev/null +++ b/.idea/dictionaries/VLE2FE.xml @@ -0,0 +1,9 @@ + + + + bcrypt + cfenv + dfopdb + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index c947305..7e46df7 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,5 +2,6 @@ \ No newline at end of file diff --git a/oas/oas.yaml b/api/api.yaml similarity index 70% rename from oas/oas.yaml rename to api/api.yaml index 81e06bf..44756ae 100644 --- a/oas/oas.yaml +++ b/api/api.yaml @@ -6,7 +6,10 @@ info: version: 1.0.0 description: | This API gives access to the project database.
- Access is restricted. Authentication can be obtained with HTTP Basic Auth using username and password. Data access methods can also be accessed using an API key at the URL ending like ?key=xxx
+ Access is restricted. Authentication can be obtained with HTTP Basic Auth using username and password. + Data access methods can also be accessed using an API key at the URL ending like ?key=xxx
+ The description lists available authentication methods, also the locks of each method close correspondingly + if the entered authentication is allowed.

There are a number of different user levels:
+ Password policy: + @@ -36,7 +48,7 @@ tags: - name: /material - name: /condition - name: /measurement - - name: /templates + - name: /template - name: /model - name: /user diff --git a/api/condition.yaml b/api/condition.yaml new file mode 100644 index 0000000..5efa2ac --- /dev/null +++ b/api/condition.yaml @@ -0,0 +1,73 @@ +/condition/{id}: + parameters: + - $ref: 'api.yaml#/components/parameters/Id' + get: + summary: TODO condition by id + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - /condition + responses: + 200: + description: condition details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Condition' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + put: + summary: TODO add/change condition + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /condition + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Condition' + responses: + 200: + description: condition details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Condition' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + delete: + summary: TODO delete condition + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /condition + security: + - BasicAuth: [] + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/api/material.yaml b/api/material.yaml new file mode 100644 index 0000000..a3b80da --- /dev/null +++ b/api/material.yaml @@ -0,0 +1,119 @@ +/materials: + get: + summary: lists all materials + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - /material + responses: + 200: + description: all material details + content: + application/json: + schema: + type: array + items: + $ref: 'api.yaml#/components/schemas/Material' + 401: + $ref: 'api.yaml#/components/responses/401' + 500: + $ref: 'api.yaml#/components/responses/500' + +/material/{id}: + parameters: + - $ref: 'api.yaml#/components/parameters/Id' + get: + summary: get material details + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - /material + responses: + 200: + description: material details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Material' + 401: + $ref: 'api.yaml#/components/responses/401' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + put: + summary: change material + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /material + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Material' + responses: + 200: + description: material details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Material' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + delete: + summary: delete material + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /material + security: + - BasicAuth: [] + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + +/material/new: + post: + summary: add material + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /material + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Material' + responses: + 200: + description: material details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Material' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 500: + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/api/measurement.yaml b/api/measurement.yaml new file mode 100644 index 0000000..0f86047 --- /dev/null +++ b/api/measurement.yaml @@ -0,0 +1,73 @@ +/measurement/{id}: + parameters: + - $ref: 'api.yaml#/components/parameters/Id' + get: + summary: TODO measurement values by id + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - /measurement + responses: + 200: + description: measurement details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Measurement' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + put: + summary: TODO add/change measurement + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /measurement + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Measurement' + responses: + 200: + description: measurement details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Measurement' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + delete: + summary: TODO delete measurement + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /measurement + security: + - BasicAuth: [] + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/api/model.yaml b/api/model.yaml new file mode 100644 index 0000000..f9c3d72 --- /dev/null +++ b/api/model.yaml @@ -0,0 +1,70 @@ +/model/{name}: + parameters: + - $ref: 'api.yaml#/components/parameters/Name' + get: + summary: TODO get model data by name + description: 'Auth: all, levels: dev, admin' + tags: + - /model + responses: + 200: + description: binary model data + content: + application/octet-stream: + schema: + type: string + format: binary + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + put: + summary: TODO add/replace model data by name + description: 'Auth: all, levels: dev, admin' + tags: + - /model + requestBody: + required: true + description: binary model data + content: + application/json: + schema: + type: string + format: binary + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + delete: + summary: TODO delete model data + description: 'Auth: basic, levels: dev, admin' + tags: + - /model + security: + - BasicAuth: [] + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/api/others.yaml b/api/others.yaml new file mode 100644 index 0000000..a953bf8 --- /dev/null +++ b/api/others.yaml @@ -0,0 +1,43 @@ +/: + get: + summary: Root method + description: 'Auth: none' + tags: + - / + security: [] + responses: + 200: + description: Server is working + content: + application/json: + schema: + properties: + status: + type: string + example: 'API server up and running!' + 500: + $ref: 'api.yaml#/components/responses/500' + +/authorized: + get: + summary: Checks authorization + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - / + responses: + 200: + description: Authorized + content: + application/json: + schema: + properties: + status: + type: string + example: 'Authorization successful' + method: + type: string + example: 'basic' + 401: + $ref: 'api.yaml#/components/responses/401' + 500: + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/parameters.yaml b/api/parameters.yaml similarity index 66% rename from oas/parameters.yaml rename to api/parameters.yaml index 659808f..ba8d046 100644 --- a/oas/parameters.yaml +++ b/api/parameters.yaml @@ -4,8 +4,10 @@ Id: required: true schema: type: string + example: 5ea0450ed851c30a90e70894 Name: name: name + description: has to be URL encoded in: path required: true schema: diff --git a/oas/responses.yaml b/api/responses.yaml similarity index 100% rename from oas/responses.yaml rename to api/responses.yaml diff --git a/api/sample.yaml b/api/sample.yaml new file mode 100644 index 0000000..32bb6ed --- /dev/null +++ b/api/sample.yaml @@ -0,0 +1,145 @@ +/samples: + get: + summary: all samples in overview + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - /sample + responses: + 200: + description: samples overview + content: + application/json: + schema: + type: array + items: + $ref: 'api.yaml#/components/schemas/SampleRefs' + 401: + $ref: 'api.yaml#/components/responses/401' + 500: + $ref: 'api.yaml#/components/responses/500' +/sample/{id}: + parameters: + - $ref: 'api.yaml#/components/parameters/Id' + get: + summary: TODO sample details + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - /sample + responses: + 200: + description: samples details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/SampleDetail' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + put: + summary: TODO change sample + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /sample + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Sample' + responses: + 200: + description: samples details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/SampleDetail' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + delete: + summary: TODO delete sample + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /sample + security: + - BasicAuth: [] + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + +/sample/new: + post: + summary: add sample + description: 'Auth: basic, levels: write, maintain, dev, admin' + tags: + - /sample + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/Sample' + responses: + 200: + description: samples details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/SampleRefs' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 500: + $ref: 'api.yaml#/components/responses/500' + +/sample/notes/fields: + get: + summary: TODO list all existing field names for custom notes fields + description: 'Auth: all, levels: read, write, maintain, dev, admin' + tags: + - /sample + responses: + 200: + description: field names and quantity of usage + content: + application/json: + schema: + properties: + name: + type: string + qty: + type: number + example: 20 + 401: + $ref: 'api.yaml#/components/responses/401' + 500: + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/api/schemas.yaml b/api/schemas.yaml new file mode 100644 index 0000000..a7aa0e2 --- /dev/null +++ b/api/schemas.yaml @@ -0,0 +1,183 @@ +Id: + type: string + example: 5ea0450ed851c30a90e70894 +_Id: + properties: + _id: + allOf: + - $ref: 'api.yaml#/components/schemas/Id' + readOnly: true +Color: + properties: + color: + type: string + example: black +SampleProperties: + properties: + number: + type: string + example: Rng172 + type: + type: string + example: granulate + batch: + type: string + example: 1560237365 + +SampleRefs: + allOf: + - $ref: 'api.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/Color' + - $ref: 'api.yaml#/components/schemas/SampleProperties' + properties: + material_id: + $ref: 'api.yaml#/components/schemas/Id' + note_id: + $ref: 'api.yaml#/components/schemas/Id' + user_id: + $ref: 'api.yaml#/components/schemas/Id' +Sample: + allOf: + - $ref: 'api.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/Color' + - $ref: 'api.yaml#/components/schemas/SampleProperties' + properties: + material_id: + allOf: + - $ref: 'api.yaml#/components/schemas/Id' + notes: + type: object + properties: + comment: + type: string + sample_references: + type: array + items: + properties: + id: + $ref: 'api.yaml#/components/schemas/Id' + relation: + type: string + example: part to this sample +SampleDetail: + allOf: + - $ref: 'api.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/Color' + - $ref: 'api.yaml#/components/schemas/SampleProperties' + properties: + material: + $ref: 'api.yaml#/components/schemas/Material' + notes: + type: object + properties: + comment: + type: string + sample_references: + type: array + items: + $ref: 'api.yaml#/components/schemas/Id' + conditions: + type: array + items: + $ref: 'api.yaml#/components/schemas/Condition' + +Material: + allOf: + - $ref: 'api.yaml#/components/schemas/_Id' + properties: + name: + type: string + example: Stanyl TW 200 F8 + supplier: + type: string + example: DSM + group: + type: string + example: PA46 + mineral: + type: number + example: 0 + glass_fiber: + type: number + example: 40 + carbon_fiber: + type: number + example: 0 + numbers: + type: array + items: + type: object + allOf: + - $ref: 'api.yaml#/components/schemas/Color' + properties: + number: + type: number + example: 5514263423 + +Condition: + allOf: + - $ref: 'api.yaml#/components/schemas/_Id' + properties: + sample_id: + $ref: 'api.yaml#/components/schemas/Id' + parameters: + type: object + treatment_template: + $ref: 'api.yaml#/components/schemas/Id' + +Measurement: + allOf: + - $ref: 'api.yaml#/components/schemas/_Id' + properties: + condition_id: + $ref: 'api.yaml#/components/schemas/Id' + values: + type: object + measurement_template: + $ref: 'api.yaml#/components/schemas/Id' + +Template: + allOf: + - $ref: 'api.yaml#/components/schemas/_Id' + properties: + name: + type: string + parameters: + type: array + items: + type: object + properties: + name: + type: string + range: + type: object + +Email: + properties: + email: + type: string + example: john.doe@bosch.com +UserName: + properties: + name: + type: string + example: johndoe +User: + allOf: + - $ref: 'api.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/UserName' + - $ref: 'api.yaml#/components/schemas/Email' + properties: + pass: + type: string + writeOnly: true + example: Abc123!# + level: + type: string + example: read + location: + type: string + example: Rng + device_name: + type: string + example: Alpha II diff --git a/api/template.yaml b/api/template.yaml new file mode 100644 index 0000000..5b362fb --- /dev/null +++ b/api/template.yaml @@ -0,0 +1,263 @@ +/template/treatments: + get: + summary: all available treatment methods + description: 'Auth: basic, levels: read, write, maintain, dev, admin' + tags: + - /template + security: + - BasicAuth: [] + responses: + 200: + description: list of treatments + content: + application/json: + schema: + type: array + items: + $ref: 'api.yaml#/components/schemas/Template' + example: + _id: 5ea0450ed851c30a90e70894 + name: heat aging + parameters: + - name: method + range: + values: + - copper + - hot air + 401: + $ref: 'api.yaml#/components/responses/401' + 500: + $ref: 'api.yaml#/components/responses/500' +/template/treatment/{name}: + parameters: + - $ref: 'api.yaml#/components/parameters/Name' + get: + summary: treatment method details + description: 'Auth: basic, levels: read, write, maintain, admin' + tags: + - /template + security: + - BasicAuth: [] + responses: + 200: + description: treatment details + content: + application/json: + schema: + allOf: + - $ref: 'api.yaml#/components/schemas/Template' + example: + _id: 5ea0450ed851c30a90e70894 + name: heat aging + parameters: + - name: method + range: + values: + - copper + - hot air + 401: + $ref: 'api.yaml#/components/responses/401' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + put: + summary: add/change treatment method + description: 'Auth: basic, levels: maintain, admin' + tags: + - /template + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: 'api.yaml#/components/schemas/Template' + example: + name: heat aging + parameters: + - name: method + range: + values: + - copper + - hot air + responses: + 200: + description: treatment details + content: + application/json: + schema: + allOf: + - $ref: 'api.yaml#/components/schemas/Template' + example: + _id: 5ea0450ed851c30a90e70894 + name: heat aging + parameters: + - name: method + range: + values: + - copper + - hot air + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + delete: + summary: delete treatment method + description: 'Auth: basic, levels: maintain, admin' + tags: + - /template + security: + - BasicAuth: [] + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' +/template/measurements: + get: + summary: all available measurement methods + description: 'Auth: basic, levels: read, write, maintain, dev, admin' + tags: + - /template + security: + - BasicAuth: [] + responses: + 200: + description: list of measurement methods + content: + application/json: + schema: + type: array + items: + $ref: 'api.yaml#/components/schemas/Template' + example: + _id: 5ea0450ed851c30a90e70894 + name: humidity + parameters: + - name: kf + range: + min: 0 + max: 2 + 401: + $ref: 'api.yaml#/components/responses/401' + 500: + $ref: 'api.yaml#/components/responses/500' +/template/measurement/{name}: + parameters: + - $ref: 'api.yaml#/components/parameters/Name' + get: + summary: measurement method details + description: 'Auth: basic, levels: read, write, maintain, admin' + tags: + - /template + security: + - BasicAuth: [] + responses: + 200: + description: measurement details + content: + application/json: + schema: + allOf: + - $ref: 'api.yaml#/components/schemas/Template' + example: + _id: 5ea0450ed851c30a90e70894 + name: humidity + parameters: + - name: kf + range: + min: 0 + max: 2 + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + put: + summary: add/change measurement method + description: 'Auth: basic, levels: maintain, admin' + tags: + - /template + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: 'api.yaml#/components/schemas/Template' + example: + _id: 5ea0450ed851c30a90e70894 + name: humidity + parameters: + - name: kf + range: + min: 0 + max: 2 + responses: + 200: + description: measurement details + content: + application/json: + schema: + allOf: + - $ref: 'api.yaml#/components/schemas/Template' + example: + _id: 5ea0450ed851c30a90e70894 + name: humidity + parameters: + - name: kf + range: + min: 0 + max: 2 + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + delete: + summary: delete measurement method + description: 'Auth: basic, levels: maintain, admin' + tags: + - /template + security: + - BasicAuth: [] + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/api/user.yaml b/api/user.yaml new file mode 100644 index 0000000..757ebf0 --- /dev/null +++ b/api/user.yaml @@ -0,0 +1,255 @@ +/users: + get: + summary: lists all users + description: 'Auth: basic, levels: admin' + tags: + - /user + security: + - BasicAuth: [] + responses: + 200: + description: user API key + content: + application/json: + schema: + type: array + items: + $ref: 'api.yaml#/components/schemas/User' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 500: + $ref: 'api.yaml#/components/responses/500' +/user: + get: + summary: list own user details + description: 'Auth: basic, levels: read, write, maintain, admin' + tags: + - /user + security: + - BasicAuth: [] + responses: + 200: + description: user details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/User' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 500: + $ref: 'api.yaml#/components/responses/500' + put: + summary: change user details + description: 'Auth: basic, levels: read, write, maintain, admin' + tags: + - /user + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: 'api.yaml#/components/schemas/_Id' + - $ref: 'api.yaml#/components/schemas/UserName' + - $ref: 'api.yaml#/components/schemas/Email' + properties: + pass: + type: string + writeOnly: true + example: Abc123!# + location: + type: string + example: Rng + device_name: + type: string + example: Alpha II + responses: + 200: + description: user details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/User' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 500: + $ref: 'api.yaml#/components/responses/500' + delete: + summary: delete user + description: 'Auth: basic, levels: read, write, maintain, admin' + tags: + - /user + security: + - BasicAuth: [] + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 401: + $ref: 'api.yaml#/components/responses/401' + 500: + $ref: 'api.yaml#/components/responses/500' +/user/{name}: + parameters: + - $ref: 'api.yaml#/components/parameters/Name' + get: + summary: list user details + description: 'Auth: basic, levels: admin' + tags: + - /user + security: + - BasicAuth: [] + responses: + 200: + description: user details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/User' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + put: + summary: change user details + description: 'Auth: basic, levels: admin' + tags: + - /user + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/User' + responses: + 200: + description: user details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/User' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' + delete: + summary: delete user + description: 'Auth: basic, levels: admin' + tags: + - /user + security: + - BasicAuth: [] + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' +/user/key: + get: + summary: get API key for the user + description: 'Auth: basic, levels: read, write, maintain, dev, admin' + tags: + - /user + security: + - BasicAuth: [] + responses: + 200: + description: user details + content: + application/json: + schema: + properties: + key: + type: string + example: 5ea0450ed851c30a90e70899 + 401: + $ref: 'api.yaml#/components/responses/401' + 500: + $ref: 'api.yaml#/components/responses/500' +/user/new: + post: + summary: add new user + description: 'Auth: basic, levels: admin' + tags: + - /user + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + required: + - email + - name + - pass + - level + - location + - device_name + allOf: + - $ref: 'api.yaml#/components/schemas/User' + responses: + 200: + description: user details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/User' + 400: + $ref: 'api.yaml#/components/responses/400' + 401: + $ref: 'api.yaml#/components/responses/401' + 403: + $ref: 'api.yaml#/components/responses/403' + 500: + $ref: 'api.yaml#/components/responses/500' +/user/passreset: + post: + summary: reset password and send mail to restore + description: 'Auth: none' + tags: + - /user + security: [] + requestBody: + required: true + description: mail saved in user profile to provide authentication + content: + application/json: + schema: + allOf: + - $ref: 'api.yaml#/components/schemas/UserName' + - $ref: 'api.yaml#/components/schemas/Email' + responses: + 200: + $ref: 'api.yaml#/components/responses/Ok' + 404: + $ref: 'api.yaml#/components/responses/404' + 500: + $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/condition.yaml b/oas/condition.yaml deleted file mode 100644 index 1259ec1..0000000 --- a/oas/condition.yaml +++ /dev/null @@ -1,69 +0,0 @@ -/condition/{id}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Id' - get: - summary: TODO condition by id - description: 'levels: read, write, maintain, dev, admin' - tags: - - /condition - responses: - 200: - description: condition details - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Condition' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - put: - summary: TODO add/change condition - description: 'levels: write, maintain, dev, admin' - tags: - - /condition - requestBody: - required: true - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Condition' - responses: - 200: - description: condition details - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Condition' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - delete: - summary: TODO delete condition - description: 'levels: write, maintain, dev, admin' - tags: - - /condition - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/material.yaml b/oas/material.yaml deleted file mode 100644 index 2ba26d7..0000000 --- a/oas/material.yaml +++ /dev/null @@ -1,63 +0,0 @@ -/material/{id}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Id' - get: - summary: TODO get material details - description: 'levels: read, write, maintain, dev, admin' - tags: - - /material - responses: - 200: - description: created material - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Material' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 500: - $ref: 'oas.yaml#/components/responses/500' - put: - summary: TODO add/change material - description: 'levels: write, maintain, dev, admin' - tags: - - /material - requestBody: - required: true - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Material' - responses: - 200: - description: material details - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Material' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 500: - $ref: 'oas.yaml#/components/responses/500' - delete: - summary: TODO delete material - description: 'levels: write, maintain, dev, admin' - tags: - - /material - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/measurement.yaml b/oas/measurement.yaml deleted file mode 100644 index 52c0430..0000000 --- a/oas/measurement.yaml +++ /dev/null @@ -1,69 +0,0 @@ -/measurement/{id}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Id' - get: - summary: TODO measurement values by id - description: 'levels: read, write, maintain, dev, admin' - tags: - - /measurement - responses: - 200: - description: measurement details - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Measurement' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - put: - summary: TODO add/change measurement - description: 'levels: write, maintain, dev, admin' - tags: - - /measurement - requestBody: - required: true - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Measurement' - responses: - 200: - description: measurement details - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Measurement' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - delete: - summary: TODO delete measurement - description: 'levels: write, maintain, dev, admin' - tags: - - /measurement - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/model.yaml b/oas/model.yaml deleted file mode 100644 index ce237e2..0000000 --- a/oas/model.yaml +++ /dev/null @@ -1,68 +0,0 @@ -/model/{name}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Name' - get: - summary: TODO get model data by name - description: 'levels: dev, admin' - tags: - - /model - responses: - 200: - description: binary model data - content: - application/octet-stream: - schema: - type: string - format: binary - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - put: - summary: TODO add/replace model data by name - description: 'levels: dev, admin' - tags: - - /model - requestBody: - required: true - description: binary model data - content: - application/json: - schema: - type: string - format: binary - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - delete: - summary: TODO delete model data - description: 'levels: dev, admin' - tags: - - /model - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/others.yaml b/oas/others.yaml deleted file mode 100644 index df322fc..0000000 --- a/oas/others.yaml +++ /dev/null @@ -1,18 +0,0 @@ -/: - get: - summary: Root method - tags: - - / - security: [] - responses: - 200: - description: Server is working - content: - application/json: - schema: - properties: - message: - type: string - example: 'API server up and running!' - 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/sample.yaml b/oas/sample.yaml deleted file mode 100644 index 8464e06..0000000 --- a/oas/sample.yaml +++ /dev/null @@ -1,108 +0,0 @@ -/samples: - get: - summary: TODO all samples in overview - description: 'levels: read, write, maintain, dev, admin' - tags: - - /sample - responses: - 200: - description: samples overview - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Samples' - 401: - $ref: 'oas.yaml#/components/responses/401' - 500: - $ref: 'oas.yaml#/components/responses/500' -/sample/{id}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Id' - get: - summary: TODO sample details - description: 'levels: read, write, maintain, dev, admin' - tags: - - /sample - responses: - 200: - description: samples details - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/SampleDetail' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - put: - summary: TODO add/change sample - description: 'levels: write, maintain, dev, admin' - tags: - - /sample - requestBody: - required: true - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Sample' - responses: - 200: - description: samples details - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/SampleDetail' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - delete: - summary: TODO delete sample - description: 'levels: write, maintain, dev, admin' - tags: - - /sample - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' -/sample/notes/fields: - get: - summary: TODO list all existing field names for custom notes fields - description: 'levels: write, maintain, dev, admin' - tags: - - /sample - responses: - 200: - description: field names and quantity of usage - content: - application/json: - schema: - properties: - name: - type: string - qty: - type: number - example: 20 - 401: - $ref: 'oas.yaml#/components/responses/401' - 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/schemas.yaml b/oas/schemas.yaml deleted file mode 100644 index 21ebae4..0000000 --- a/oas/schemas.yaml +++ /dev/null @@ -1,164 +0,0 @@ -Id: - type: string -_Id: - properties: - _id: - allOf: - - $ref: 'oas.yaml#/components/schemas/Id' - readOnly: true -Color: - properties: - color: - type: string -SampleProperties: - properties: - sample_number: - type: string - type: - type: string - batch: - type: string - validated: - type: boolean - -Samples: - allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - - $ref: 'oas.yaml#/components/schemas/Color' - - $ref: 'oas.yaml#/components/schemas/SampleProperties' - properties: - material_id: - $ref: 'oas.yaml#/components/schemas/Id' - note_id: - $ref: 'oas.yaml#/components/schemas/Id' - user_id: - $ref: 'oas.yaml#/components/schemas/Id' -Sample: - allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - - $ref: 'oas.yaml#/components/schemas/Color' - - $ref: 'oas.yaml#/components/schemas/SampleProperties' - properties: - material: - $ref: 'oas.yaml#/components/schemas/Material' - notes: - type: object - properties: - comments: - type: string - sample_references: - type: array - items: - $ref: 'oas.yaml#/components/schemas/Id' -SampleDetail: - allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - - $ref: 'oas.yaml#/components/schemas/Color' - - $ref: 'oas.yaml#/components/schemas/SampleProperties' - properties: - material: - $ref: 'oas.yaml#/components/schemas/Material' - notes: - type: object - properties: - comments: - type: string - sample_references: - type: array - items: - $ref: 'oas.yaml#/components/schemas/Id' - conditions: - type: array - items: - $ref: 'oas.yaml#/components/schemas/Condition' - -Material: - allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - properties: - material_numbers: - type: array - items: - type: object - allOf: - - $ref: 'oas.yaml#/components/schemas/Color' - properties: - number: - type: number - material_group: - type: string - supplier: - type: string - material_name: - type: string - mineral: - type: number - glass_fiber: - type: number - carbon_fiber: - type: number - -Condition: - allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - properties: - sample_id: - $ref: 'oas.yaml#/components/schemas/Id' - parameters: - type: object - treatment_template: - $ref: 'oas.yaml#/components/schemas/Id' - -Measurement: - allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - properties: - condition_id: - $ref: 'oas.yaml#/components/schemas/Id' - values: - type: object - measurement_template: - $ref: 'oas.yaml#/components/schemas/Id' - -Template: - allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - properties: - name: - type: string - parameters: - type: array - items: - type: object - properties: - name: - type: string - range: - type: object - -Email: - required: - - email - properties: - email: - type: string - example: john.doe@bosch.com -User: - allOf: - - $ref: 'oas.yaml#/components/schemas/_Id' - - $ref: 'oas.yaml#/components/schemas/Email' - properties: - name: - type: string - example: johndoe - levels: - type: array - items: - type: string - example: read - location: - type: string - example: Rng - device_name: - type: string - example: Alpha II \ No newline at end of file diff --git a/oas/template.yaml b/oas/template.yaml deleted file mode 100644 index bce58d0..0000000 --- a/oas/template.yaml +++ /dev/null @@ -1,242 +0,0 @@ -/template/treatments: - get: - summary: TODO all available treatment methods - description: 'levels: read, write, maintain, dev, admin' - tags: - - /templates - security: - - BasicAuth: [] - responses: - 200: - description: list of treatments - content: - application/json: - schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/Template' - example: - name: heat aging - parameters: - - name: method - range: - - copper - 401: - $ref: 'oas.yaml#/components/responses/401' - 500: - $ref: 'oas.yaml#/components/responses/500' -/templates/treatment/{name}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Name' - get: - summary: TODO treatment method details - description: 'levels: read, write, maintain, admin' - tags: - - /templates - security: - - BasicAuth: [] - responses: - 200: - description: treatment details - content: - application/json: - schema: - allOf: - - $ref: 'oas.yaml#/components/schemas/Template' - example: - name: heat aging - parameters: - - name: method - range: - - copper - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - put: - summary: TODO add/change treatment method - description: 'levels: maintain, admin' - tags: - - /templates - requestBody: - required: true - content: - application/json: - schema: - allOf: - - $ref: 'oas.yaml#/components/schemas/Template' - example: - name: heat aging - parameters: - - name: method - range: - - copper - responses: - 200: - description: treatment details - content: - application/json: - schema: - allOf: - - $ref: 'oas.yaml#/components/schemas/Template' - example: - name: heat aging - parameters: - - name: method - range: - - copper - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - delete: - summary: TODO delete treatment method - description: 'levels: maintain, admin' - tags: - - /templates - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' -/template/measurements: - get: - summary: TODO all available measurement methods - description: 'levels: read, write, maintain, dev, admin' - tags: - - /templates - security: - - BasicAuth: [] - responses: - 200: - description: list of measurement methods - content: - application/json: - schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/Template' - example: - name: humidity - parameters: - - name: kf - range: - min: 0 - max: 2 - 401: - $ref: 'oas.yaml#/components/responses/401' - 500: - $ref: 'oas.yaml#/components/responses/500' -/templates/measurement/{name}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Name' - get: - summary: TODO measurement method details - description: 'levels: read, write, maintain, admin' - tags: - - /templates - security: - - BasicAuth: [] - responses: - 200: - description: measurement details - content: - application/json: - schema: - allOf: - - $ref: 'oas.yaml#/components/schemas/Template' - example: - name: humidity - parameters: - - name: kf - range: - min: 0 - max: 2 - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - put: - summary: TODO add/change measurement method - description: 'levels: maintain, admin' - tags: - - /templates - requestBody: - required: true - content: - application/json: - schema: - allOf: - - $ref: 'oas.yaml#/components/schemas/Template' - example: - name: humidity - parameters: - - name: kf - range: - min: 0 - max: 2 - responses: - 200: - description: measurement details - content: - application/json: - schema: - allOf: - - $ref: 'oas.yaml#/components/schemas/Template' - example: - name: humidity - parameters: - - name: kf - range: - min: 0 - max: 2 - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - delete: - summary: TODO delete measurement method - description: 'levels: maintain, admin' - tags: - - /templates - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/oas/user.yaml b/oas/user.yaml deleted file mode 100644 index c9f10b1..0000000 --- a/oas/user.yaml +++ /dev/null @@ -1,170 +0,0 @@ -/users: - get: - summary: TODO lists all users - description: 'levels: admin' - tags: - - /user - security: - - BasicAuth: [] - responses: - 200: - description: user API key - content: - application/json: - schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/User' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 500: - $ref: 'oas.yaml#/components/responses/500' -/user/{name}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Name' - get: - summary: TODO list user details - description: 'levels: read, write, maintain, dev get their own information without a name property specified, level: admin can get any user using the name parameter' - tags: - - /user - security: - - BasicAuth: [] - responses: - 200: - description: user details - content: - application/json: - schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/User' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - put: - summary: TODO change user details - description: 'levels: read, write, maintain, dev can change their own information (except level) without a name property specified, level: admin can change any user using the name parameter' - tags: - - /user - requestBody: - required: true - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/User' - responses: - 200: - description: user details - content: - application/json: - schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/User' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' - delete: - summary: TODO delete user - description: 'levels: read, write, maintain, dev can delete their own account, level: admin can delete any user using the name parameter' - tags: - - /user - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' - 500: - $ref: 'oas.yaml#/components/responses/500' -/user/key: - get: - summary: TODO get API key for the user - description: 'levels: read, write, maintain, dev, admin' - tags: - - /user - security: - - BasicAuth: [] - responses: - 200: - description: user details - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/User' - 401: - $ref: 'oas.yaml#/components/responses/401' - 500: - $ref: 'oas.yaml#/components/responses/500' -/user/new: - post: - summary: TODO add new user - description: 'levels: admin' - tags: - - /user - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/User' - responses: - 200: - description: user details - content: - application/json: - schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/User' - 400: - $ref: 'oas.yaml#/components/responses/400' - 401: - $ref: 'oas.yaml#/components/responses/401' - 403: - $ref: 'oas.yaml#/components/responses/403' - 500: - $ref: 'oas.yaml#/components/responses/500' -/user/passreset: - post: - summary: TODO reset password and send mail to restore - tags: - - /user - security: [] - requestBody: - required: true - description: mail saved in user profile to provide authentication - content: - application/json: - schema: - $ref: 'oas.yaml#/components/schemas/Email' - responses: - 200: - $ref: 'oas.yaml#/components/responses/Ok' - 401: - $ref: 'oas.yaml#/components/responses/401' - 500: - $ref: 'oas.yaml#/components/responses/500' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ae887b0..839b669 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,49 @@ "js-tokens": "^4.0.0" } }, + "@hapi/address": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.0.1.tgz", + "integrity": "sha512-0oEP5UiyV4f3d6cBL8F3Z5S7iWSX39Knnl0lY8i+6gfmmIBj44JCBNtcMgwyS+5v7j3VYavNay0NFHDS+UGQcw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@hapi/formula": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz", + "integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==" + }, + "@hapi/hoek": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.0.4.tgz", + "integrity": "sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw==" + }, + "@hapi/joi": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-17.1.1.tgz", + "integrity": "sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg==", + "requires": { + "@hapi/address": "^4.0.1", + "@hapi/formula": "^2.0.0", + "@hapi/hoek": "^9.0.0", + "@hapi/pinpoint": "^2.0.0", + "@hapi/topo": "^5.0.0" + } + }, + "@hapi/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==" + }, + "@hapi/topo": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", + "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@jsdevtools/ono": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz", @@ -50,21 +93,102 @@ "defer-to-connect": "^1.0.1" } }, + "@types/bcrypt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-3.0.0.tgz", + "integrity": "sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ==" + }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bson": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.2.tgz", + "integrity": "sha512-+uWmsejEHfmSjyyM/LkrP0orfE2m5Mx9Xel4tXNeqi1ldK5XMQcDsFkBmLDtuyKUbxj2jGDo0H240fbCRJZo7Q==", + "requires": { + "@types/node": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz", + "integrity": "sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw==", + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + }, "@types/mocha": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==" }, + "@types/mongodb": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.10.tgz", + "integrity": "sha512-6NkJNfFdFa/njBvN/9eAfq78bWUnapkdR3JbWGGpd7U71PjgKweA4Tlag8psi2mqm973vBYVTD1oc1u0lzRcig==", + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, + "@types/mongoose": { + "version": "5.7.12", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.7.12.tgz", + "integrity": "sha512-yzLJk3cdSwuMXaIacUCWUb8m960YcgnID7S4ZPOOgzT39aSC46670TuunN+ajDio7OUcGG4mGg8eOGs2Z6VmrA==", + "requires": { + "@types/mongodb": "*", + "@types/node": "*" + } + }, "@types/node": { "version": "13.1.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.6.tgz", "integrity": "sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg==" }, + "@types/qs": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", + "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", + "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -160,11 +284,32 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, "binary-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", @@ -514,6 +659,11 @@ "safe-buffer": "5.1.2" } }, + "content-filter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/content-filter/-/content-filter-1.1.2.tgz", + "integrity": "sha512-VaZ4Y7h776r0v2WxWqu3iatjYI6/N0msXK8O1ymtkFWbSvaFoCePksS8U60BS6dUMZeAlqhN09SuM7ghdzRP1Q==" + }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -645,9 +795,9 @@ } }, "es-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", - "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -787,6 +937,24 @@ "is-buffer": "~2.0.3" } }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "form-data": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", @@ -1181,12 +1349,12 @@ "dev": true }, "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", "dev": true, "requires": { - "chalk": "^2.0.1" + "chalk": "^2.4.2" } }, "lowercase-keys": { @@ -1275,9 +1443,9 @@ } }, "mocha": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.0.0.tgz", - "integrity": "sha512-CirsOPbO3jU86YKjjMzFLcXIb5YiGLUrjrXFHoJ3e2z9vWiaZVCZQ2+gtRGMPWF+nFhN6AWwLM/juzAQ6KRkbA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz", + "integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==", "dev": true, "requires": { "ansi-colors": "3.2.3", @@ -1291,9 +1459,9 @@ "growl": "1.10.5", "he": "1.2.0", "js-yaml": "3.13.1", - "log-symbols": "2.2.0", + "log-symbols": "3.0.0", "minimatch": "3.0.4", - "mkdirp": "0.5.1", + "mkdirp": "0.5.5", "ms": "2.1.1", "node-environment-flags": "1.0.6", "object.assign": "4.1.0", @@ -1301,8 +1469,8 @@ "supports-color": "6.0.0", "which": "1.3.1", "wide-align": "1.1.3", - "yargs": "13.3.0", - "yargs-parser": "13.1.1", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", "yargs-unparser": "1.6.0" }, "dependencies": { @@ -1351,21 +1519,6 @@ "path-is-absolute": "^1.0.0" } }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -1392,6 +1545,11 @@ } } }, + "mongo-sanitize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mongo-sanitize/-/mongo-sanitize-1.1.0.tgz", + "integrity": "sha512-6gB9AiJD+om2eZLxaPKIP5Q8P3Fr+s+17rVWso7hU0+MAzmIvIMlgTYuyvalDLTtE/p0gczcvJ8A3pbN1XmQ/A==" + }, "mongodb": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.4.1.tgz", @@ -1586,9 +1744,9 @@ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" }, "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -2009,24 +2167,46 @@ "strip-ansi": "^4.0.0" } }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" } }, "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, "string_decoder": { @@ -2459,9 +2639,9 @@ "dev": true }, "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { "cliui": "^5.0.0", @@ -2473,7 +2653,7 @@ "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" + "yargs-parser": "^13.1.2" }, "dependencies": { "ansi-regex": { @@ -2505,21 +2685,13 @@ } }, "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } } }, "yargs-unparser": { diff --git a/package.json b/package.json index 5c89ff2..d3f9e63 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "description": "API for the digital fingerprint of plastics mongodb", "main": "index.js", "scripts": { + "tsc": "tsc", "test": "mocha dist/**/**.spec.js", - "start": "tsc && node dist/index.js", + "start": "tsc && node dist/index.js || exit 1", "dev": "nodemon -e ts,yaml --exec \"npm run start\"" }, "keywords": [], @@ -13,11 +14,24 @@ "license": "ISC", "dependencies": { "@apidevtools/json-schema-ref-parser": "^8.0.0", + "@hapi/joi": "^17.1.1", + "@types/bcrypt": "^3.0.0", + "@types/body-parser": "^1.19.0", + "@types/express-serve-static-core": "^4.17.5", "@types/mocha": "^5.2.7", + "@types/mongoose": "^5.7.12", "@types/node": "^13.1.6", + "@types/qs": "^6.9.1", + "@types/serve-static": "^1.13.3", + "axios": "^0.19.2", + "basic-auth": "^2.0.1", + "bcryptjs": "^2.4.3", + "body-parser": "^1.19.0", "cfenv": "^1.2.2", + "content-filter": "^1.1.2", "express": "^4.17.1", "json-schema": "^0.2.5", + "mongo-sanitize": "^1.1.0", "mongoose": "^5.8.7", "nodemon": "^2.0.3", "swagger-ui-express": "^4.1.2", @@ -25,7 +39,7 @@ "typescript": "^3.7.4" }, "devDependencies": { - "mocha": "^7.0.0", + "mocha": "^7.1.2", "should": "^13.2.3", "supertest": "^4.0.2" } diff --git a/src/customTypes/express.ts b/src/customTypes/express.ts new file mode 100644 index 0000000..361c961 --- /dev/null +++ b/src/customTypes/express.ts @@ -0,0 +1,116 @@ +// Type definitions for Express 4.17 +// Project: http://expressjs.com +// Definitions by: Boris Yankov +// China Medical University Hospital +// Puneet Arora +// Dylan Frankland +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.3 + +/* =================== USAGE =================== + + import * as express from "express"; + var app = express(); + + =============================================== */ + +/// +/// + +import * as bodyParser from "body-parser"; +import serveStatic = require("serve-static"); +import * as core from "express-serve-static-core"; +import * as qs from "qs"; + +/** + * Creates an Express application. The express() function is a top-level function exported by the express module. + */ +declare function e(): core.Express; + +declare namespace e { + /** + * This is a built-in middleware function in Express. It parses incoming requests with JSON payloads and is based on body-parser. + * @since 4.16.0 + */ + var json: typeof bodyParser.json; + + /** + * This is a built-in middleware function in Express. It parses incoming requests with Buffer payloads and is based on body-parser. + * @since 4.17.0 + */ + var raw: typeof bodyParser.raw; + + /** + * This is a built-in middleware function in Express. It parses incoming requests with text payloads and is based on body-parser. + * @since 4.17.0 + */ + var text: typeof bodyParser.text; + + /** + * These are the exposed prototypes. + */ + var application: Application; + var request: Request; + var response: Response; + + /** + * This is a built-in middleware function in Express. It serves static files and is based on serve-static. + */ + var static: typeof serveStatic; + + /** + * This is a built-in middleware function in Express. It parses incoming requests with urlencoded payloads and is based on body-parser. + * @since 4.16.0 + */ + var urlencoded: typeof bodyParser.urlencoded; + + /** + * This is a built-in middleware function in Express. It parses incoming request query parameters. + */ + export function query(options: qs.IParseOptions | typeof qs.parse): Handler; + + export function Router(options?: RouterOptions): core.Router; + + interface RouterOptions { + /** + * Enable case sensitivity. + */ + caseSensitive?: boolean; + + /** + * Preserve the req.params values from the parent router. + * If the parent and the child have conflicting param names, the child’s value take precedence. + * + * @default false + * @since 4.5.0 + */ + mergeParams?: boolean; + + /** + * Enable strict routing. + */ + strict?: boolean; + } + + interface Application extends core.Application { } + interface CookieOptions extends core.CookieOptions { } + interface Errback extends core.Errback { } + interface ErrorRequestHandler

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

extends core.Request { } + interface RequestHandler

extends core.RequestHandler { } + interface RequestParamHandler extends core.RequestParamHandler { } + export interface Response extends core.Response { } + interface Router extends core.Router { } + interface Send extends core.Send { } +} + +export = e; diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 0000000..f188468 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,109 @@ +import mongoose from 'mongoose'; +import cfenv from 'cfenv'; + +// mongoose.set('debug', true); // enable mongoose debug + +// database urls, prod db url is retrieved automatically +const TESTING_URL = 'mongodb://localhost/dfopdb_test'; +const DEV_URL = 'mongodb://localhost/dfopdb'; + +export default class db { + private static state = { // db object and current mode (test, dev, prod) + db: null, + mode: null, + }; + + static connect (mode = '', done: Function = () => {}) { // set mode to test for unit/integration tests, otherwise skip parameter. done is also only needed for testing + if (this.state.db) return done(); // db is already connected + + // find right connection url + let connectionString: string = ""; + if (mode === 'test') { // testing + connectionString = TESTING_URL; + this.state.mode = 'test'; + } + else if(process.env.NODE_ENV === 'production') { + let services = cfenv.getAppEnv().getServices(); + for (let service in services) { + if(services[service].tags.indexOf("mongodb") >= 0) { + connectionString = services[service]["credentials"].uri; + } + } + this.state.mode = 'prod'; + } + else { + connectionString = DEV_URL; + this.state.mode = 'dev'; + } + + // connect to db + mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true, connectTimeoutMS: 10000}, err => { + if (err) done(err); + }); + mongoose.connection.on('error', console.error.bind(console, 'connection error:')); + mongoose.connection.on('disconnected', () => { // reset state on disconnect + console.log('Database disconnected'); + this.state.db = 0; + done(); + }); + process.on('SIGINT', () => { // close connection when app is terminated + mongoose.connection.close(() => { + console.log('Mongoose default connection disconnected through app termination'); + process.exit(0); + }); + }); + mongoose.connection.once('open', () => { + mongoose.set('useFindAndModify', false); + console.log(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`); + this.state.db = mongoose.connection; + done(); + }); + } + + static getState () { + return this.state; + } + + static drop (done: Function = () => {}) { // drop all collections of connected db (only dev and test for safety reasons ;) + if (!this.state.db || this.state.mode === 'prod') return done(); // no db connection or prod db + this.state.db.db.listCollections().toArray((err, collections) => { // get list of all collections + if (collections.length === 0) { // there are no collections to drop + return done(); + } + else { + let dropCounter = 0; // count number of dropped collections to know when to return done() + collections.forEach(collection => { // drop each collection + this.state.db.dropCollection(collection.name, () => { + if (++ dropCounter >= collections.length) { // all collections dropped + done(); + } + }); + }); + } + }); + } + + static loadJson (json, done: Function = () => {}) { // insert given JSON data into db, uses core mongodb methods + if (!this.state.db || !json.hasOwnProperty('collections') || json.collections.length === 0) { + return done(); + } // no db connection or nothing to load + + let loadCounter = 0; // count number of loaded collections to know when to return done() + Object.keys(json.collections).forEach(collectionName => { // create each collection + for(let i in json.collections[collectionName]) { // convert $oid fields to actual ObjectIds + Object.keys(json.collections[collectionName][i]).forEach(key => { + if (json.collections[collectionName][i][key] !== null && json.collections[collectionName][i][key].hasOwnProperty('$oid')) { + json.collections[collectionName][i][key] = mongoose.Types.ObjectId(json.collections[collectionName][i][key].$oid); + } + }) + } + this.state.db.createCollection(collectionName, (err, collection) => { + collection.insertMany(json.collections[collectionName], () => { // insert JSON data + if (++ loadCounter >= Object.keys(json.collections).length) { // all collections loaded + done(); + } + }); + }); + }); + } +}; \ No newline at end of file diff --git a/src/globals.ts b/src/globals.ts new file mode 100644 index 0000000..0d4ccdb --- /dev/null +++ b/src/globals.ts @@ -0,0 +1,11 @@ +const globals = { + levels: [ // access levels + 'read', + 'write', + 'maintain', + 'dev', + 'admin' + ] +}; + +export default globals; \ No newline at end of file diff --git a/src/helpers/authorize.ts b/src/helpers/authorize.ts new file mode 100644 index 0000000..e2f626a --- /dev/null +++ b/src/helpers/authorize.ts @@ -0,0 +1,101 @@ +import basicAuth from 'basic-auth'; +import bcrypt from 'bcryptjs'; +import UserModel from '../models/user'; + + +// appends req.auth(res, ['levels'], method = 'all') +// which returns sends error message and returns false if unauthorized, otherwise true +// req.authDetails returns eg. {methods: ['basic'], username: 'johndoe', level: 'write'} + +module.exports = async (req, res, next) => { + let givenMethod = ''; // authorization method given by client, basic taken preferred + let user = {name: '', level: '', id: ''}; // user object + + // test authentications + const userBasic = await basic(req, next); + + if (userBasic) { // basic available + givenMethod = 'basic'; + user = userBasic; + } + else { // if basic not available, test key + const userKey = await key(req, next); + if (userKey) { + givenMethod = 'key'; + user = userKey; + } + } + + req.auth = (res, levels, method = 'all') => { + if (givenMethod === method || (method === 'all' && givenMethod !== '')) { // method is available + if (levels.indexOf(user.level) > -1) { // level is available + return true; + } + else { + res.status(403).json({status: 'Forbidden'}); + return false; + } + } + else { + res.status(401).json({status: 'Unauthorized'}); + return false; + } + } + + req.authDetails = { + method: givenMethod, + username: user.name, + level: user.level, + id: user.id + }; + + next(); +} + + +function basic (req, next): any { // checks basic auth and returns changed user object + return new Promise(resolve => { + const auth = basicAuth(req); + if (auth !== undefined) { // basic auth available + UserModel.find({name: auth.name}).lean().exec( (err, data: any) => { // find user + if (err) return next(err); + if (data.length === 1) { // one user found + bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password + if (err) return next(err); + if (res === true) { + resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString()}); + } + else { + resolve(null); + } + }); + } + else { + resolve(null); + } + }); + } + else { + resolve(null); + } + }); +} + +function key (req, next): any { // checks API key and returns changed user object + return new Promise(resolve => { + if (req.query.key !== undefined) { + UserModel.find({key: req.query.key}).lean().exec( (err, data: any) => { // find user + if (err) return next(err); + if (data.length === 1) { // one user found + resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString()}); + } + else { + resolve(null); + } + }); + } + else { + resolve(null); + } + }); +} \ No newline at end of file diff --git a/src/helpers/mail.ts b/src/helpers/mail.ts new file mode 100644 index 0000000..949d243 --- /dev/null +++ b/src/helpers/mail.ts @@ -0,0 +1,64 @@ +import axios from 'axios'; + +// sends an email + +export default (mailAddress, subject, content, f) => { // callback, executed empty or with error + if (process.env.NODE_ENV === 'production') { + const mailService = JSON.parse(process.env.VCAP_SERVICES).Mail[0]; + axios({ + method: 'post', + url: mailService.credentials.uri + '/email', + auth: {username: mailService.credentials.username, password: mailService.credentials.password}, + data: { + recipients: [{to: mailAddress}], + subject: {content: subject}, + body: { + content: content, + contentType: "text/html" + }, + from: { + eMail: "dfop@bosch-iot.com", + password: "PlasticsOfFingerprintDigital" + } + } + }) + .then(() => { + f(); + }) + .catch((err) => { + f(err); + }); + } + else if (process.env.NODE_ENV === 'test') { + console.log('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content); + f(); + } + else { // dev + axios({ + method: 'get', + url: 'https://digital-fingerprint-of-plastics-mail-test.apps.de1.bosch-iot-cloud.com/api', + data: { + method: 'post', + url: '/email', + data: { + recipients: [{to: mailAddress}], + subject: {content: subject}, + body: { + content: content, + contentType: "text/html" + }, + from: { + eMail: "dfop-test@bosch-iot.com", + password: "PlasticsOfFingerprintDigital" + } + } + } + }) + .then(() => { + f(); + }) + .catch((err) => { + f(err); + }); + } +} \ No newline at end of file diff --git a/src/helpers/test.ts b/src/helpers/test.ts new file mode 100644 index 0000000..6c2fa72 --- /dev/null +++ b/src/helpers/test.ts @@ -0,0 +1,90 @@ +import supertest from 'supertest'; +import should from 'should/as-function'; +import db from "../db"; + + +export default class TestHelper { + public static auth = { + admin: {pass: 'Abc123!#', key: '000000000000000000001003'}, + janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002'}, + user: {pass: 'Xyz890*)', key: '000000000000000000001001'} + } + public static res = { + 400: {status: 'Bad request'}, + 401: {status: 'Unauthorized'}, + 403: {status: 'Forbidden'}, + 404: {status: 'Not found'}, + 500: {status: 'Internal server error'} + } + + static before (done) { + process.env.port = '2999'; + process.env.NODE_ENV = 'test'; + db.connect('test', done); + } + + static beforeEach (server, done) { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + return server + } + + static afterEach (server, done) { + server.close(done); + } + + static request (server, done, options) { // options in form: {method, url, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res} + let st = supertest(server); + if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) { + options.url += '?key=' + (this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key); + } + switch (options.method) { + case 'get': + st = st.get(options.url) + break; + case 'post': + st = st.post(options.url) + break; + case 'put': + st = st.put(options.url) + break; + case 'delete': + st = st.delete(options.url) + break; + } + if (options.hasOwnProperty('req')) { + st = st.send(options.req); + } + if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('basic')) { + if (this.auth.hasOwnProperty(options.auth.basic)) { + st = st.auth(options.auth.basic, this.auth[options.auth.basic].pass) + } + else { + st = st.auth(options.auth.basic.name, options.auth.basic.pass) + } + } + st = st.expect('Content-type', /json/) + .expect(options.httpStatus); + if (options.hasOwnProperty('res')) { + return st.end((err, res) => { + if (err) return done (err); + should(res.body).be.eql(options.res); + done(); + }); + } + else if (this.res.hasOwnProperty(options.httpStatus) && options.default !== false) { + return st.end((err, res) => { + if (err) return done (err); + should(res.body).be.eql(this.res[options.httpStatus]); + done(); + }); + } + else { + return st; + } + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 09fb57f..63ca19e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,37 +1,18 @@ -import cfenv from 'cfenv'; import express from 'express'; -import mongoose from 'mongoose'; +import bodyParser from 'body-parser'; import swagger from 'swagger-ui-express'; import jsonRefParser, {JSONSchema} from '@apidevtools/json-schema-ref-parser'; +import contentFilter from 'content-filter'; +import mongoSanitize from 'mongo-sanitize'; +import db from './db'; // tell if server is running in debug or production environment -console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : '===== DEVELOPMENT ====='); - - -// get mongodb address from server, otherwise set to localhost -let connectionString: string = ""; -if(process.env.NODE_ENV === 'production') { - let services = cfenv.getAppEnv().getServices(); - for (let service in services) { - if(services[service].tags.indexOf("mongodb") >= 0) { - connectionString = services[service]["credentials"].uri; - } - } -} -else { - connectionString = 'mongodb://localhost/dfopdb'; -} -mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true}); - -// connect to mongodb -let db = mongoose.connection; -db.on('error', console.error.bind(console, 'connection error:')); -db.once('open', () => { - console.log(`Connected to ${connectionString}`); -}); +console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT ====='); +// mongodb connection +db.connect(); // create Express app const app = express(); @@ -40,20 +21,61 @@ app.disable('x-powered-by'); // get port from environment, defaults to 3000 const port = process.env.PORT || 3000; +//middleware +app.use(express.json({ limit: '5mb'})); +app.use(express.urlencoded({ extended: false, limit: '5mb' })); +app.use(bodyParser.json()); +app.use(contentFilter()); // filter URL query attacks +app.use((req, res, next) => { // filter body query attacks + req.body = mongoSanitize(req.body); + next(); +}); +app.use((err, req, res, ignore) => { // bodyParser error handling + res.status(400).send({status: 'Invalid JSON body'}); +}); +app.use((req, res, next) => { // no database connection error + if (db.getState().db) { + next(); + } + else { + res.status(500).send({status: 'Internal server error'}); + } +}); +app.use(require('./helpers/authorize')); // handle authentication + // require routes app.use('/', require('./routes/root')); +app.use('/', require('./routes/sample')); +app.use('/', require('./routes/material')); +app.use('/', require('./routes/template')); +app.use('/', require('./routes/user')); + +// static files +app.use('/static', express.static('static')); // Swagger UI -let oasDoc: JSONSchema = {}; -jsonRefParser.bundle('oas/oas.yaml', (err, doc) => { +let apiDoc: JSONSchema = {}; +jsonRefParser.bundle('api/api.yaml', (err, doc) => { if(err) throw err; - oasDoc = doc; - oasDoc.paths = oasDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); - swagger.setup(oasDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'}); + apiDoc = doc; + apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); + swagger.setup(apiDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'}); }); -app.use('/api', swagger.serve, swagger.setup(oasDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'})); +app.use('/api', swagger.serve, swagger.setup(apiDoc, {customCssUrl: '/static/styles/swagger.css'})); + +app.use((req, res) => { // 404 error handling + res.status(404).json({status: 'Not found'}); +}); + +app.use((err, req, res, ignore) => { // internal server error handling + console.error(err); + res.status(500).json({status: 'Internal server error'}); +}); + // hook up server to port -app.listen(port, () => { - console.log(`Listening on http;//localhost:${port}`); +const server = app.listen(port, () => { + console.log(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`); }); + +module.exports = server; \ No newline at end of file diff --git a/src/models/material.ts b/src/models/material.ts new file mode 100644 index 0000000..530f8f0 --- /dev/null +++ b/src/models/material.ts @@ -0,0 +1,16 @@ +import mongoose from 'mongoose'; + +const MaterialSchema = new mongoose.Schema({ + name: {type: String, index: {unique: true}}, + supplier: String, + group: String, + mineral: String, + glass_fiber: String, + carbon_fiber: String, + numbers: [{ + color: String, + number: Number + }] +}); + +export default mongoose.model('material', MaterialSchema); \ No newline at end of file diff --git a/src/models/measurement_template.ts b/src/models/measurement_template.ts new file mode 100644 index 0000000..c55cbc7 --- /dev/null +++ b/src/models/measurement_template.ts @@ -0,0 +1,11 @@ +import mongoose from 'mongoose'; + +const MeasurementTemplateSchema = new mongoose.Schema({ + name: {type: String, index: {unique: true}}, + parameters: [{ + name: String, + range: mongoose.Schema.Types.Mixed + }] +}, {minimize: false}); // to allow empty objects + +export default mongoose.model('measurement_template', MeasurementTemplateSchema); \ No newline at end of file diff --git a/src/models/note.ts b/src/models/note.ts new file mode 100644 index 0000000..a13fd6a --- /dev/null +++ b/src/models/note.ts @@ -0,0 +1,12 @@ +import mongoose from 'mongoose'; + +const NoteSchema = new mongoose.Schema({ + comment: String, + sample_references: [{ + id: mongoose.Schema.Types.ObjectId, + relation: String + }], + custom_fields: mongoose.Schema.Types.Mixed +}); + +export default mongoose.model('note', NoteSchema); \ No newline at end of file diff --git a/src/models/note_field.ts b/src/models/note_field.ts new file mode 100644 index 0000000..86158e3 --- /dev/null +++ b/src/models/note_field.ts @@ -0,0 +1,8 @@ +import mongoose from 'mongoose'; + +const NoteFieldSchema = new mongoose.Schema({ + name: {type: String, index: {unique: true}}, + qty: Number +}); + +export default mongoose.model('note_field', NoteFieldSchema); \ No newline at end of file diff --git a/src/models/sample.ts b/src/models/sample.ts new file mode 100644 index 0000000..81dcc28 --- /dev/null +++ b/src/models/sample.ts @@ -0,0 +1,18 @@ +import mongoose from 'mongoose'; + +import MaterialModel from './material'; +import NoteModel from './note'; +import UserModel from './user'; + +const SampleSchema = new mongoose.Schema({ + number: {type: String, index: {unique: true}}, + type: String, + color: String, + batch: String, + validated: Boolean, + material_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialModel}, + note_id: {type: mongoose.Schema.Types.ObjectId, ref: NoteModel}, + user_id: {type: mongoose.Schema.Types.ObjectId, ref: UserModel} +}); + +export default mongoose.model('sample', SampleSchema); \ No newline at end of file diff --git a/src/models/treatment_template.ts b/src/models/treatment_template.ts new file mode 100644 index 0000000..3b61164 --- /dev/null +++ b/src/models/treatment_template.ts @@ -0,0 +1,11 @@ +import mongoose from 'mongoose'; + +const TreatmentTemplateSchema = new mongoose.Schema({ + name: {type: String, index: {unique: true}}, + parameters: [{ + name: String, + range: mongoose.Schema.Types.Mixed + }] +}, {minimize: false}); // to allow empty objects + +export default mongoose.model('treatment_template', TreatmentTemplateSchema); \ No newline at end of file diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..50178a6 --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,13 @@ +import mongoose from 'mongoose'; + +const UserSchema = new mongoose.Schema({ + name: {type: String, index: {unique: true}}, + email: String, + pass: String, + key: String, + level: String, + location: String, + device_name: String +}); + +export default mongoose.model('user', UserSchema); \ No newline at end of file diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts new file mode 100644 index 0000000..7b84c08 --- /dev/null +++ b/src/routes/material.spec.ts @@ -0,0 +1,397 @@ +import should from 'should/as-function'; +import MaterialModel from '../models/material'; +import TestHelper from "../helpers/test"; + + +describe('/material', () => { + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + + describe('GET /materials', () => { + it('returns all materials', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.materials.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(material).have.property('_id').be.type('string'); + should(material).have.property('name').be.type('string'); + should(material).have.property('supplier').be.type('string'); + should(material).have.property('group').be.type('string'); + should(material).have.property('mineral').be.type('number'); + should(material).have.property('glass_fiber').be.type('number'); + should(material).have.property('carbon_fiber').be.type('number'); + should(material.numbers).matchEach(number => { + should(number).have.only.keys('color', 'number'); + should(number).have.property('color').be.type('string'); + should(number).have.property('number').be.type('number'); + }); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials', + auth: {key: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.materials.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(material).have.property('_id').be.type('string'); + should(material).have.property('name').be.type('string'); + should(material).have.property('supplier').be.type('string'); + should(material).have.property('group').be.type('string'); + should(material).have.property('mineral').be.type('number'); + should(material).have.property('glass_fiber').be.type('number'); + should(material).have.property('carbon_fiber').be.type('number'); + should(material.numbers).matchEach(number => { + should(number).have.only.keys('color', 'number'); + should(number).have.property('color').be.type('string'); + should(number).have.property('number').be.type('number'); + }); + }); + done(); + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials', + httpStatus: 401 + }); + }); + }); + + describe('GET /material/{id}', () => { + it('returns the right material', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}, {color: 'natural', number: 5514263422}]} + }); + }); + it('returns the right material for an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000003', + auth: {key: 'admin'}, + httpStatus: 200, + res: {_id: '100000000000000000000003', name: 'PA GF 50 black (2706)', supplier: 'Akro-Plastic', group: 'PA66+PA6I/6T', mineral: 0, glass_fiber: 0, carbon_fiber: 0, numbers: []} + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/10000000000000000000000x', + auth: {key: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects an unknown id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000111', + auth: {key: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000001', + httpStatus: 401 + }); + }); + }); + + describe('PUT /material/{id}', () => { + it('returns the right material', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {}, + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}, {color: 'natural', number: 5514263422}]} + }); + }); + it('keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]}, + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]} + }); + }); + it('changes the given properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]} + , + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]}); + MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => { + if (err) return done(err); + data._id = data._id.toString({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]}); + data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}}); + should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: '0', glass_fiber: '35', carbon_fiber: '0', numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}], __v: 0} + ); + done(); + }); + }); + }); + it('rejects already existing material names', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Ultramid T KR 4355 G7'}, + res: {status: 'Material name already taken'} + }); + }); + it('rejects wrong material properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {mineral: 'x', glass_fiber: 'x', carbon_fiber: 'x', numbers: [{colorxx: 'black', number: 'xxx'}]}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/10000000000000000000000x', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {}, + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000002', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000002', + auth: {basic: 'user'}, + httpStatus: 403, + req: {} + }); + }); + it('returns 404 for an unknown material', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000111', + auth: {basic: 'janedoe'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + httpStatus: 401, + req: {} + }); + }); + }); + + describe('DELETE /material/{id}', () => { + it('deletes the material', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { + if (err) return done(err); + should(data).be.null(); + done(); + }); + }); + }); + it('rejects deleting a material referenced by samples'); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/10000000000000000000000x', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000002', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000002', + auth: {basic: 'user'}, + httpStatus: 403 + }); + }); + it('returns 404 for an unknown id', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000111', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000001', + httpStatus: 401 + }); + }); + }); + + describe('POST /material/new', () => { + it('returns the right material', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('name', 'Crastin CE 2510'); + should(res.body).have.property('supplier', 'Du Pont'); + should(res.body).have.property('group', 'PBT'); + should(res.body).have.property('mineral', 0); + should(res.body).have.property('glass_fiber', 30); + should(res.body).have.property('carbon_fiber', 0); + should(res.body.numbers).matchEach(number => { + should(number).have.only.keys('color', 'number'); + should(number).have.property('color', 'black'); + should(number).have.property('number', 5515798402); + }); + done(); + }); + }); + it('stores the material', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + }).end(err => { + if (err) return done (err); + MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'Crastin CE 2510'); + should(data[0]).have.property('supplier', 'Du Pont'); + should(data[0]).have.property('group', 'PBT'); + should(data[0]).have.property('mineral', '0'); + should(data[0]).have.property('glass_fiber', '30'); + should(data[0]).have.property('carbon_fiber', '0'); + should(data[0].numbers).have.lengthOf(0); + done(); + }); + }); + }); + it('rejects already existing material names', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]}, + res: {status: 'Material name already taken'} + }); + }); + it('rejects wrong material properties', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 'x', glass_fiber: 'x', carbon_fiber: 'x', numbers: [{colorxx: 'black', number: 'xxx'}]}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects incomplete material properties', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'user'}, + httpStatus: 403, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + httpStatus: 401, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + }); + }); + }); +}); \ No newline at end of file diff --git a/src/routes/material.ts b/src/routes/material.ts new file mode 100644 index 0000000..5628fa6 --- /dev/null +++ b/src/routes/material.ts @@ -0,0 +1,110 @@ +import express from 'express'; + +import MaterialValidate from './validate/material'; +import MaterialModel from '../models/material' +import IdValidate from './validate/id'; + + +const router = express.Router(); + +router.get('/materials', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + + MaterialModel.find({}).lean().exec((err, data) => { + if (err) return next(err); + res.json(data.map(e => MaterialValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }); +}); + +router.get('/material/' + IdValidate.parameter(), (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + + MaterialModel.findById(req.params.id).lean().exec((err, data) => { + if (err) return next(err); + if (data) { + res.json(MaterialValidate.output(data)); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + +router.put('/material/' + IdValidate.parameter(), (req, res, next) => { + if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + + const {error, value: material} = MaterialValidate.input(req.body, 'change'); + if (error) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + if (material.hasOwnProperty('name')) { + MaterialModel.find({name: material.name}).lean().exec((err, data) => { + if (err) return next(err); + if (data.length > 0 && data[0]._id != req.params.id) { + res.status(400).json({status: 'Material name already taken'}); + return; + } + else { + f(); + } + }); + } + else { + f(); + } + + function f() { // to resolve async + MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).lean().exec((err, data) => { + if (err) return next(err); + if (data) { + res.json(MaterialValidate.output(data)); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); + } +}); + +router.delete('/material/' + IdValidate.parameter(), (req, res, next) => { + if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + + MaterialModel.findByIdAndDelete(req.params.id).lean().exec((err, data) => { + if (err) return next(err); + if (data) { + res.json({status: 'OK'}) + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + +router.post('/material/new', (req, res, next) => { + if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + + // validate input + const {error, value: material} = MaterialValidate.input(req.body, 'new'); + if (error) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + MaterialModel.find({name: material.name}).lean().exec((err, data) => { + if (err) return next(err); + if (data.length > 0) { + res.status(400).json({status: 'Material name already taken'}); + return; + } + + new MaterialModel(material).save((err, data) => { + if (err) return next(err); + res.json(MaterialValidate.output(data.toObject())); + }); + }); +}); + + +module.exports = router; \ No newline at end of file diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index cfec79c..25be1ba 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -1,19 +1,69 @@ -import supertest from 'supertest'; -import should from 'should/as-function'; +import TestHelper from "../helpers/test"; -let server = supertest.agent('http://localhost:3000'); +describe('/', () => { + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); -describe('Testing /', () => { - it('returns the message object', done => { - server - .get('/') - .expect('Content-type', /json/) - .expect(200) - .end(function(err, res) { - should(res.statusCode).equal(200); - should(res.body).be.eql({message: 'API server up and running!'}); - done(); + describe('GET /', () => { + it('returns the root message', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/', + httpStatus: 200, + res: {status: 'API server up and running!'} }); + }); }); -}); + + describe('Unknown routes', () => { + it('return a 404 message', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/unknownroute', + httpStatus: 404 + }); + }); + }); + + describe('An unauthorized request', () => { + it('returns a 401 message', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + httpStatus: 401 + }); + }); + it('does not work with correct username', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {name: 'admin', pass: 'Abc123!!'}, + httpStatus: 401 + }); + }); + }); + + describe('An authorized request', () => { + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {key: 'admin'}, + httpStatus: 200, + res: {status: 'Authorization successful', method: 'key'} + }); + }); + it('works with basic auth', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {status: 'Authorization successful', method: 'basic'} + }); + }); + }); +}); \ No newline at end of file diff --git a/src/routes/root.ts b/src/routes/root.ts index 896f360..2705280 100644 --- a/src/routes/root.ts +++ b/src/routes/root.ts @@ -1,9 +1,15 @@ import express from 'express'; +import globals from '../globals'; const router = express.Router(); router.get('/', (req, res) => { - res.json({message: 'API server up and running!'}); + res.json({status: 'API server up and running!'}); +}); + +router.get('/authorized', (req, res) => { + if (!req.auth(res, globals.levels)) return; + res.json({status: 'Authorization successful', method: req.authDetails.method}); }); module.exports = router; diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts new file mode 100644 index 0000000..857556c --- /dev/null +++ b/src/routes/sample.spec.ts @@ -0,0 +1,336 @@ +import should from 'should/as-function'; +import SampleModel from '../models/sample'; +import NoteModel from '../models/note'; +import NoteFieldModel from '../models/note_field'; +import TestHelper from "../helpers/test"; + + +describe('/sample', () => { + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + + describe('GET /samples', () => { + it('returns all samples', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id'); + should(material).have.property('_id').be.type('string'); + should(material).have.property('number').be.type('string'); + should(material).have.property('type').be.type('string'); + should(material).have.property('color').be.type('string'); + should(material).have.property('batch').be.type('string'); + should(material).have.property('material_id').be.type('string'); + should(material).have.property('note_id'); + should(material).have.property('user_id').be.type('string'); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples', + auth: {key: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id'); + should(material).have.property('_id').be.type('string'); + should(material).have.property('number').be.type('string'); + should(material).have.property('type').be.type('string'); + should(material).have.property('color').be.type('string'); + should(material).have.property('batch').be.type('string'); + should(material).have.property('material_id').be.type('string'); + should(material).have.property('note_id'); + should(material).have.property('user_id').be.type('string'); + }); + done(); + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples', + httpStatus: 401 + }); + }); + }); + + describe('POST /sample/new', () => { + it('returns the right sample', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('number', 'Rng172'); + should(res.body).have.property('color', 'black'); + should(res.body).have.property('type', 'granulate'); + should(res.body).have.property('batch', '1560237365'); + should(res.body).have.property('material_id', '100000000000000000000001'); + should(res.body).have.property('note_id').be.type('string'); + should(res.body).have.property('user_id', '000000000000000000000002'); + done(); + }); + }); + it('stores the sample', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + }).end(err => { + if (err) return done (err); + SampleModel.find({number: 'Rng172'}).lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('number', 'Rng172'); + should(data[0]).have.property('color', 'black'); + should(data[0]).have.property('type', 'granulate'); + should(data[0]).have.property('batch', '1560237365'); + should(data[0].material_id.toString()).be.eql('100000000000000000000001'); + should(data[0].user_id.toString()).be.eql('000000000000000000000002'); + should(data[0]).have.property('note_id'); + NoteModel.findById(data[0].note_id).lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.property('_id'); + should(data).have.property('comment', 'Testcomment'); + should(data).have.property('sample_references'); + should(data.sample_references).have.lengthOf(1); + should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003'); + should(data.sample_references[0]).have.property('relation', 'part to this sample'); + done(); + }); + }) + }); + }); + it('stores the custom fields', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [], custom_fields: {field1: 'a', field2: 'b', 'not allowed for new applications': true}}} + }).end((err, res) => { + if (err) return done (err); + NoteModel.findById(res.body.note_id).lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('_id'); + should(data).have.property('comment', 'Testcomment'); + should(data).have.property('sample_references').have.lengthOf(0); + should(data).have.property('custom_fields'); + should(data.custom_fields).have.property('field1', 'a'); + should(data.custom_fields).have.property('field2', 'b'); + should(data.custom_fields).have.property('not allowed for new applications', true); + NoteFieldModel.find({name: 'field1'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('qty', 1); + NoteFieldModel.find({name: 'field2'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('qty', 1); + NoteFieldModel.find({name: 'not allowed for new applications'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('qty', 3); + done(); + }); + }); + }); + }); + }); + }); + it('rejects a color not defined for the material', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'green', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Color not available for material'} + }); + }); + it('rejects an unknown material id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '000000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Material not available'} + }); + }); + it('rejects a sample number in use', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: '1', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Sample number already taken'} + }); + }); + it('rejects an invalid sample reference', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '000000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Sample reference not available'} + }); + }); + it('rejects a missing color', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects a missing sample number', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects a missing type', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects a missing batch', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects a missing material id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid material id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'user'}, + httpStatus: 403, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + httpStatus: 401, + req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + }); + }); + }); + + describe('GET /sample/notes/fields', () => { + it('returns all fields', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/notes/fields', + auth: {basic: 'user'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.note_fields.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('name', 'qty'); + should(material).have.property('qty').be.type('number'); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/notes/fields', + auth: {key: 'user'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.note_fields.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('name', 'qty'); + should(material).have.property('qty').be.type('number'); + }); + done(); + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/notes/fields', + httpStatus: 401 + }); + }); + }); +}); diff --git a/src/routes/sample.ts b/src/routes/sample.ts new file mode 100644 index 0000000..bbebaba --- /dev/null +++ b/src/routes/sample.ts @@ -0,0 +1,109 @@ +import express from 'express'; + +import SampleValidate from './validate/sample'; +import NoteFieldValidate from './validate/note_field'; +import SampleModel from '../models/sample' +import MaterialModel from '../models/material'; +import NoteModel from '../models/note'; +import NoteFieldModel from '../models/note_field'; + + + +const router = express.Router(); + +router.get('/samples', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + + SampleModel.find({}).lean().exec((err, data) => { + if (err) return next(err); + res.json(data.map(e => SampleValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }) +}); + + +router.post('/sample/new', (req, res, next) => { + if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + + const {error, value: sample} = SampleValidate.input(req.body, 'new'); + if (error) { + return res.status(400).json({status: 'Invalid body format'}); + } + + MaterialModel.findById(sample.material_id).lean().exec((err, data: any) => { // validate material_id + if (err) return next(err); + if (!data) { // could not find material_id + return res.status(400).json({status: 'Material not available'}); + } + if (!data.numbers.find(e => e.color === sample.color)) { // color for material not specified + return res.status(400).json({status: 'Color not available for material'}); + } + SampleModel.findOne({number: sample.number}).lean().exec((err, data) => { // validate sample number + if (err) return next(err); + if (data) { // found entry with sample number + return res.status(400).json({status: 'Sample number already taken'}); + } + + if (sample.notes.sample_references.length > 0) { // validate sample_references + let referencesCount = sample.notes.sample_references.length; + sample.notes.sample_references.forEach(reference => { + SampleModel.findById(reference.id).lean().exec((err, data) => { + if (err) return next(err); + if (!data) { + return res.status(400).json({status: 'Sample reference not available'}); + } + referencesCount --; + if (referencesCount <= 0) { + f(); + } + }); + }); + } + else { + f(); + } + + if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { + customFieldsAdd(Object.keys(sample.notes.custom_fields)); + } + + function f() { // to resolve async + new NoteModel(sample.notes).save((err, data) => { + if (err) return next(err); + delete sample.notes; + sample.note_id = data._id; + sample.user_id = req.authDetails.id; + new SampleModel(sample).save((err, data) => { + if (err) return next(err); + res.json(SampleValidate.output(data.toObject())); + }); + }); + } + }); + }) +}); + +router.get('/sample/notes/fields', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + + NoteFieldModel.find({}).lean().exec((err, data) => { + if (err) return next(err); + res.json(data.map(e => NoteFieldValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }) +}); + + +module.exports = router; + + +function customFieldsAdd (fields) { + fields.forEach(field => { + NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: 1}}).lean().exec((err, data) => { // check if field exists + if (err) return console.error(err); + if (!data) { // new field + new NoteFieldModel({name: field, qty: 1}).save(err => { + if (err) return console.error(err); + }) + } + }); + }); +} \ No newline at end of file diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts new file mode 100644 index 0000000..68b3d4a --- /dev/null +++ b/src/routes/template.spec.ts @@ -0,0 +1,578 @@ +import should from 'should/as-function'; +import TemplateTreatmentModel from '../models/treatment_template'; +import TemplateMeasurementModel from '../models/measurement_template'; +import TestHelper from "../helpers/test"; + + +describe('/template', () => { + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + + describe('/template/treatment', () => { + describe('GET /template/treatments', () => { + it('returns all treatment templates', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatments', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.treatment_templates.length); + should(res.body).matchEach(treatment => { + should(treatment).have.only.keys('_id', 'name', 'parameters'); + should(treatment).have.property('_id').be.type('string'); + should(treatment).have.property('name').be.type('string'); + should(treatment.parameters).matchEach(number => { + should(number).have.only.keys('name', 'range'); + should(number).have.property('name').be.type('string'); + should(number).have.property('range').be.type('object'); + }); + }); + done(); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatments', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatments', + httpStatus: 401 + }); + }); + }); + + describe('GET /template/treatment/{name}', () => { + it('returns the right treatment template', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatment/heat%20treatment', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects an unknown name', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatment/xxx', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/treatment/heat%20treatment', + httpStatus: 401 + }); + }); + }); + + describe('PUT /template/treatment/{name}', () => { + it('returns the right treatment template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + }); + }); + it('keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + }); + }); + it('changes the given properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '200000000000000000000001', name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}); + TemplateTreatmentModel.find({name: 'heat aging'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'parameters'); + should(data[0]).have.property('name', 'heat aging'); + should(data[0]).have.property('parameters').have.lengthOf(1); + should(data[0].parameters[0]).have.property('name', 'time'); + should(data[0].parameters[0]).have.property('range'); + should(data[0].parameters[0].range).have.property('min', 1); + done(); + }); + }); + }); + it('supports values ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {values: [1, 2, 5]}}]}, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'time', range: {values: [1, 2, 5]}}]} + }); + }); + it('supports min max ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {min: 1, max: 11}}]}, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'time', range: {min: 1, max: 11}}]} + }); + }); + it('supports empty ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {}}]}, + res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'time', range: {}}]} + }); + }); + it('adds a new template for an unknown name', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20aging', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }).end(err => { + if (err) return done(err); + TemplateTreatmentModel.find({name: 'heat aging'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'parameters', '__v'); + should(data[0]).have.property('name', 'heat aging'); + should(data[0].parameters[0]).have.property('name', 'time'); + should(data[0].parameters[0]).have.property('range'); + should(data[0].parameters[0].range).have.property('min', 1); + done(); + }); + }); + }); + it('rejects an incomplete template for a new name', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20aging', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'time'}]}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects already existing names', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat treatment 2', parameters: [{name: 'time', range: {min: 1}}]}, + res: {status: 'Template name already taken'} + }); + }); + it('rejects wrong properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20aging', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'time'}], xx: 33}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/heat%20treatment', + httpStatus: 401, + req: {} + }); + }); + }); + + describe('DELETE /template/treatment/{name}', () => { + it('deletes the template', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + TemplateTreatmentModel.find({name: 'heat treatment'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('rejects deleting a template still in use'); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/treatment/heat%20treatment', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/treatment/heat%20treatment', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }) + }); + it('returns 404 for an unknown name', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/treatment/xxx', + auth: {basic: 'admin'}, + httpStatus: 404 + }) + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/treatment/heat%20treatment', + httpStatus: 401 + }) + }); + }); + }); + + describe('/template/measurement', () => { + describe('GET /template/measurements', () => { + it('returns all measurement templates', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurements', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.measurement_templates.length); + should(res.body).matchEach(measurement => { + should(measurement).have.only.keys('_id', 'name', 'parameters'); + should(measurement).have.property('_id').be.type('string'); + should(measurement).have.property('name').be.type('string'); + should(measurement.parameters).matchEach(number => { + should(number).have.only.keys('name', 'range'); + should(number).have.property('name').be.type('string'); + should(number).have.property('range').be.type('object'); + }); + }); + done(); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurements', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurements', + httpStatus: 401 + }); + }); + }); + + describe('GET /template/measurement/{name}', () => { + it('returns the right measurement template', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurement/spectrum', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurement/spectrum', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects an unknown name', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurement/xxx', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurement/spectrum', + httpStatus: 401 + }); + }); + }); + + describe('PUT /template/measurement/{name}', () => { + it('returns the right measurement template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]} + }); + }); + it('keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'spectrum', parameters: [{name: 'dpt', range: {}}]}, + res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]} + }); + }); + it('changes the given properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}, + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '300000000000000000000001', name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}); + TemplateMeasurementModel.find({name: 'IR spectrum'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'parameters'); + should(data[0]).have.property('name', 'IR spectrum'); + should(data[0]).have.property('parameters').have.lengthOf(1); + should(data[0].parameters[0]).have.property('name', 'data point table'); + should(data[0].parameters[0]).have.property('range'); + should(data[0].parameters[0].range).have.property('min', 0); + should(data[0].parameters[0].range).have.property('max', 1000); + done(); + }); + }); + }); + it('supports values ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]}, + res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]} + }); + }); + it('supports min max ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]}, + res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]} + }); + }); + it('supports empty ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/kf', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'weight %', range: {}}]}, + res: {_id: '300000000000000000000002', name: 'kf', parameters: [{name: 'weight %', range: {}}]} + }); + }); + it('adds a new template for an unknown name', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/vz', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]} + }).end(err => { + if (err) return done(err); + TemplateMeasurementModel.find({name: 'vz'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'parameters', '__v'); + should(data[0]).have.property('name', 'vz'); + should(data[0]).have.property('parameters').have.lengthOf(1); + should(data[0].parameters[0]).have.property('name', 'vz'); + should(data[0].parameters[0]).have.property('range'); + should(data[0].parameters[0].range).have.property('min', 1); + done(); + }); + }); + }); + it('rejects an incomplete template for a new name', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/vz', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'vz'}]}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects already existing names', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'kf', parameters: [{name: 'dpt', range: {min: 1}}]}, + res: {status: 'Template name already taken'} + }); + }); + it('rejects wrong properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'dpt'}], xx: 33}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/spectrum', + httpStatus: 401, + req: {} + }); + }); + }); + + describe('DELETE /template/measurement/{name}', () => { + it('deletes the template', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/measurement/spectrum', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + TemplateMeasurementModel.find({name: 'spectrum'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('rejects deleting a template still in use'); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/measurement/spectrum', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/measurement/spectrum', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }) + }); + it('returns 404 for an unknown name', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/measurement/xxx', + auth: {basic: 'admin'}, + httpStatus: 404 + }) + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/template/measurement/spectrum', + httpStatus: 401 + }) + }); + }); + }); +}); \ No newline at end of file diff --git a/src/routes/template.ts b/src/routes/template.ts new file mode 100644 index 0000000..1e859cd --- /dev/null +++ b/src/routes/template.ts @@ -0,0 +1,90 @@ +import express from 'express'; + +import TemplateValidate from './validate/template'; +import TemplateTreatmentModel from '../models/treatment_template'; +import TemplateMeasurementModel from '../models/measurement_template'; + + +const router = express.Router(); + +router.get('/template/:collection(measurements|treatments)', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + + (req.params.collection === 'treatments' ? TemplateTreatmentModel : TemplateMeasurementModel) + .find({}).lean().exec((err, data) => { + if (err) next (err); + res.json(data.map(e => TemplateValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }); +}); + +router.get('/template/:collection(measurement|treatment)/:name', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + + (req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel) + .findOne({name: req.params.name}).lean().exec((err, data) => { + if (err) next (err); + if (data) { + res.json(TemplateValidate.output(data)); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + +router.put('/template/:collection(measurement|treatment)/:name', (req, res, next) => { + if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + + const collectionModel = req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel; + + collectionModel.findOne({name: req.params.name}).lean().exec((err, data) => { + if (err) next (err); + const templateState = data? 'change': 'new'; + const {error, value: template} = TemplateValidate.input(req.body, templateState); + if (error) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + if (template.hasOwnProperty('name') && template.name !== req.params.name) { + collectionModel.find({name: template.name}).lean().exec((err, data) => { + if (err) next (err); + if (data.length > 0) { + res.status(400).json({status: 'Template name already taken'}); + return; + } + else { + f(); + } + }); + } + else { + f(); + } + + function f() { // to resolve async + collectionModel.findOneAndUpdate({name: req.params.name}, template, {new: true, upsert: true}).lean().exec((err, data) => { + if (err) return next(err); + res.json(TemplateValidate.output(data)); + }); + } + }); +}); + +router.delete('/template/:collection(measurement|treatment)/:name', (req, res, next) => { + if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; + + (req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel) + .findOneAndDelete({name: req.params.name}).lean().exec((err, data) => { + if (err) return next(err); + if (data) { + res.json({status: 'OK'}) + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + + +module.exports = router; \ No newline at end of file diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts new file mode 100644 index 0000000..b103ef7 --- /dev/null +++ b/src/routes/user.spec.ts @@ -0,0 +1,626 @@ +import should from 'should/as-function'; +import UserModel from '../models/user'; +import TestHelper from "../helpers/test"; + + +describe('/user', () => { + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + + describe('GET /users', () => { + it('returns all users', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.users.length); + should(res.body).matchEach(user => { + should(user).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(user).have.property('_id').be.type('string'); + should(user).have.property('email').be.type('string'); + should(user).have.property('name').be.type('string'); + should(user).have.property('level').be.type('string'); + should(user).have.property('location').be.type('string'); + should(user).have.property('device_name').be.type('string'); + }); + done(); + }); + }); + it('rejects requests from non-admins', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + httpStatus: 401 + }); + }); + }); + + describe('GET /user/{name}', () => { + it('returns own user details', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('returns other user details for admin', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('rejects requests from non-admins for another user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/admin', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects requests from a user API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/janedoe', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('returns 404 for an unknown user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/unknown', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/janedoe', + httpStatus: 401 + }); + }); + }); + + describe('PUT /user/{name}', () => { + it('returns own user details', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('returns other user details for admin', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('changes user details as given', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', device_name: 'test'} + }).end(err => { + if (err) return done (err); + UserModel.find({name: 'adminnew'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'adminnew'); + should(data[0]).have.property('email', 'adminnew@bosch.com'); + should(data[0]).have.property('pass').not.eql('Abc123##'); + should(data[0]).have.property('level', 'admin'); + should(data[0]).have.property('location', 'Abt'); + should(data[0]).have.property('device_name', 'test'); + done(); + }); + }); + }); + it('lets the admin change a user level', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {level: 'read'} + }).end(err => { + if (err) return done (err); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('level', 'read'); + done(); + }); + }); + }); + it('does not change the level', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 400, default: false, + req: {level: 'read'} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('level', 'write'); + done(); + }); + }); + }); + it('rejects a username already in use', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {name: 'janedoe'} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'Username already taken'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + done(); + }); + }); + }); + it('rejects a username which is in the special names', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}, + res: {status: 'Username already taken'} + }); + }); + it('rejects invalid user details', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', location: 44, device_name: 'Alpha II'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid email address', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid password', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {pass: 'password'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects requests from non-admins for another user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/admin', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects requests from a user API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('returns 404 for an unknown user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/unknown', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + httpStatus: 401, + req: {} + }); + }); + }); + + describe('DELETE /user/{name}', () => { + it('deletes own user details', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('deletes other user details for admin', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('rejects requests from non-admins for another user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/admin', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects requests from a user API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/janedoe', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('returns 404 for an unknown user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/unknown', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/janedoe', + httpStatus: 401 + }); + }); + }); + + describe('GET /user/key', () => { + it('returns the right API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {key: TestHelper.auth.janedoe.key} + }); + }); + it('rejects requests from an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects requests from an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + httpStatus: 401 + }); + }); + }); + + describe('POST /user/new', () => { + it('returns the added user data', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'john.doe@bosch.com'); + should(res.body).have.property('name', 'johndoe'); + should(res.body).have.property('level', 'read'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha II'); + done(); + }); + }); + it('stores the data', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }).end(err => { + if (err) return done (err); + UserModel.find({name: 'johndoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'johndoe'); + should(data[0]).have.property('email', 'john.doe@bosch.com'); + should(data[0]).have.property('pass').not.eql('Abc123!#'); + should(data[0]).have.property('level', 'read'); + should(data[0]).have.property('location', 'Rng'); + should(data[0]).have.property('device_name', 'Alpha II'); + done(); + }); + }); + }); + it('rejects a username already in use', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {email: 'j.doe@bosch.com', name: 'janedoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'Username already taken'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + done(); + }); + }); + }); + it('rejects a username which is in the special names', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}, + res: {status: 'Username already taken'} + }); + }); + it('rejects invalid user details', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 44, device_name: 'Alpha II'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid user level', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', device_name: 'Alpha II'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid email address', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects an invalid password', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'}, + res: {status: 'Invalid body format'} + }); + }); + it('rejects requests from non-admins', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {key: 'admin'}, + httpStatus: 401, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + httpStatus: 401, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'} + }); + }); + }); + + describe('POST /user/passreset', () => { + it('returns the ok response', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 200, + req: {email: 'jane.doe@bosch.com', name: 'janedoe'}, + res: {status: 'OK'} + }); + }); + it('returns 404 for wrong username/email combo', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 404, + req: {email: 'jane.doe@bosch.com', name: 'admin'} + }); + }); + it('returns 404 for unknown username', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 404, + req: {email: 'jane.doe@bosch.com', name: 'username'} + }); + }); + it('changes the user password', done => { + UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { + if (err) return done(err); + const oldpass = data[0].pass; + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 200, + req: {email: 'jane.doe@bosch.com', name: 'janedoe'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { + if (err) return done(err); + should(data[0].pass).not.eql(oldpass); + done(); + }); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts new file mode 100644 index 0000000..a0161f9 --- /dev/null +++ b/src/routes/user.ts @@ -0,0 +1,173 @@ +import express from 'express'; +import mongoose from 'mongoose'; +import bcrypt from 'bcryptjs'; + +import UserValidate from './validate/user'; +import UserModel from '../models/user'; +import mail from '../helpers/mail'; + +const router = express.Router(); + + +router.get('/users', (req, res) => { + if (!req.auth(res, ['admin'], 'basic')) return; + + UserModel.find({}).lean().exec( (err, data:any) => { + res.json(data.map(e => UserValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }); +}); + +router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex + req.params.username = req.params[0]; + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + let username = req.authDetails.username; + if (req.params.username !== undefined) { + if (!req.auth(res, ['admin'], 'basic')) return; + username = req.params.username; + } + + UserModel.findOne({name: username}).lean().exec( (err, data:any) => { + if (err) return next(err); + if (data) { + res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + +router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new + req.params.username = req.params[0]; + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + let username = req.authDetails.username; + if (req.params.username !== undefined) { + if (!req.auth(res, ['admin'], 'basic')) return; + username = req.params.username; + } + const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : '')); + if (error) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + if (user.hasOwnProperty('pass')) { + user.pass = bcrypt.hashSync(user.pass, 10); + } + + // check that user does not already exist if new name was specified + if (user.hasOwnProperty('name') && user.name !== username) { + UserModel.find({name: user.name}).lean().exec( (err, data:any) => { + if (err) return next(err); + if (data.length > 0 || UserValidate.isSpecialName(user.name)) { + res.status(400).json({status: 'Username already taken'}); + return; + } + + UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => { + if (err) return next(err); + if (data) { + res.json(UserValidate.output(data)); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); + }); + } + else { + UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => { + if (err) return next(err); + if (data) { + res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + } + else { + res.status(404).json({status: 'Not found'}); + } + }); + } +}); + +router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex + req.params.username = req.params[0]; + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + let username = req.authDetails.username; + if (req.params.username !== undefined) { + if (!req.auth(res, ['admin'], 'basic')) return; + username = req.params.username; + } + + UserModel.findOneAndDelete({name: username}).lean().exec( (err, data:any) => { + if (err) return next(err); + if (data) { + res.json({status: 'OK'}) + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + +router.get('/user/key', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + + UserModel.findOne({name: req.authDetails.username}).lean().exec( (err, data:any) => { + if (err) return next(err); + res.json({key: data.key}); + }); +}); + +router.post('/user/new', (req, res, next) => { + if (!req.auth(res, ['admin'], 'basic')) return; + + // validate input + const {error, value: user} = UserValidate.input(req.body, 'new'); + if (error) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + // check that user does not already exist + UserModel.find({name: user.name}).lean().exec( (err, data:any) => { + if (err) return next(err); + if (data.length > 0 || UserValidate.isSpecialName(user.name)) { + res.status(400).json({status: 'Username already taken'}); + return; + } + + user.key = mongoose.Types.ObjectId(); // use object id as unique API key + bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing + user.pass = hash; + new UserModel(user).save((err, data) => { // store user + if (err) return next(err); + res.json(UserValidate.output(data.toObject())); + }); + }); + }); +}); + +router.post('/user/passreset', (req, res, next) => { + // check if user/email combo exists + UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( (err, data: any) => { + if (err) return next(err); + if (data.length === 1) { // it exists + const newPass = Math.random().toString(36).substring(2); + bcrypt.hash(newPass, 10, (err, hash) => { // password hashing + if (err) return next(err); + UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}, err => { // write new password + if (err) return next(err); + mail(data[0].email, 'Your new password for the DFOP database', 'Hi,

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

' + newPass + '

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

Have a nice day.

The DFOP team', err => { + if (err) return next(err); + res.json({status: 'OK'}); + }); + }); + }); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); + + +module.exports = router; \ No newline at end of file diff --git a/src/routes/validate/id.ts b/src/routes/validate/id.ts new file mode 100644 index 0000000..5409993 --- /dev/null +++ b/src/routes/validate/id.ts @@ -0,0 +1,26 @@ +import joi from '@hapi/joi'; + +export default class IdValidate { + private static id = joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24); + + static get () { + return this.id; + } + + static valid (id) { + return this.id.validate(id).error === undefined; + } + + static parameter () { // :id url parameter + return ':id([0-9a-f]{24})'; + } + + static stringify (data) { + Object.keys(data).forEach(key => { + if (data[key] !== null && data[key].hasOwnProperty('_bsontype') && data[key]._bsontype === 'ObjectID') { + data[key] = data[key].toString(); + } + }); + return data; + } +} \ No newline at end of file diff --git a/src/routes/validate/material.ts b/src/routes/validate/material.ts new file mode 100644 index 0000000..54cd749 --- /dev/null +++ b/src/routes/validate/material.ts @@ -0,0 +1,82 @@ +import joi from '@hapi/joi'; + +import IdValidate from './id'; + +export default class MaterialValidate { // validate input for material + private static material = { + name: joi.string() + .max(128), + + supplier: joi.string() + .max(128), + + group: joi.string() + .max(128), + + mineral: joi.number() + .integer() + .min(0) + .max(100), + + glass_fiber: joi.number() + .integer() + .min(0) + .max(100), + + carbon_fiber: joi.number() + .integer() + .min(0) + .max(100), + + numbers: joi.array() + .items(joi.object({ + color: joi.string() + .max(128), + number: joi.number() + .min(0) + })) + }; + + static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated) + if (param === 'new') { + return joi.object({ + name: this.material.name.required(), + supplier: this.material.supplier.required(), + group: this.material.group.required(), + mineral: this.material.mineral.required(), + glass_fiber: this.material.glass_fiber.required(), + carbon_fiber: this.material.carbon_fiber.required(), + numbers: this.material.numbers + }).validate(data); + } + else if (param === 'change') { + return joi.object({ + name: this.material.name, + supplier: this.material.supplier, + group: this.material.group, + mineral: this.material.mineral, + glass_fiber: this.material.glass_fiber, + carbon_fiber: this.material.carbon_fiber, + numbers: this.material.numbers + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } + + static output (data) { // validate output from database for needed properties, strip everything else + data = IdValidate.stringify(data); + const {value, error} = joi.object({ + _id: IdValidate.get(), + name: this.material.name, + supplier: this.material.supplier, + group: this.material.group, + mineral: this.material.mineral, + glass_fiber: this.material.glass_fiber, + carbon_fiber: this.material.carbon_fiber, + numbers: this.material.numbers + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} \ No newline at end of file diff --git a/src/routes/validate/note_field.ts b/src/routes/validate/note_field.ts new file mode 100644 index 0000000..4892f22 --- /dev/null +++ b/src/routes/validate/note_field.ts @@ -0,0 +1,18 @@ +import joi from '@hapi/joi'; + +export default class NoteFieldValidate { + private static note_field = { + name: joi.string() + .max(128), + + qty: joi.number() + }; + + static output (data) { + const {value, error} = joi.object({ + name: this.note_field.name, + qty: this.note_field.qty + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} \ No newline at end of file diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts new file mode 100644 index 0000000..d94cede --- /dev/null +++ b/src/routes/validate/sample.ts @@ -0,0 +1,77 @@ +import joi from '@hapi/joi'; + +import IdValidate from './id'; + +export default class SampleValidate { + private static sample = { + number: joi.string() + .max(128), + + color: joi.string() + .max(128), + + type: joi.string() + .max(128), + + batch: joi.string() + .max(128) + .allow(''), + + notes: joi.object({ + comment: joi.string() + .max(512), + + sample_references: joi.array() + .items(joi.object({ + id: IdValidate.get(), + + relation: joi.string() + .max(128) + })), + + custom_fields: joi.object() + .pattern(/.*/, joi.alternatives() + .try( + joi.string().max(128), + joi.number(), + joi.boolean(), + joi.date() + ) + ) + }) + }; + + static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated) + if (param === 'new') { + return joi.object({ + number: this.sample.number.required(), + color: this.sample.color.required(), + type: this.sample.type.required(), + batch: this.sample.batch.required(), + material_id: IdValidate.get().required(), + notes: this.sample.notes.required() + }).validate(data); + } + else if (param === 'change') { + return{error: 'Not implemented!', value: {}}; + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } + + static output (data) { + data = IdValidate.stringify(data); + const {value, error} = joi.object({ + _id: IdValidate.get(), + number: this.sample.number, + color: this.sample.color, + type: this.sample.type, + batch: this.sample.batch, + material_id: IdValidate.get(), + note_id: IdValidate.get().allow(null), + user_id: IdValidate.get() + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} \ No newline at end of file diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts new file mode 100644 index 0000000..a279dce --- /dev/null +++ b/src/routes/validate/template.ts @@ -0,0 +1,59 @@ +import joi from '@hapi/joi'; +import IdValidate from './id'; + +export default class TemplateValidate { + private static template = { + name: joi.string() + .max(128), + + parameters: joi.array() + .min(1) + .items( + joi.object({ + name: joi.string() + .max(128) + .required(), + + range: joi.object({ + values: joi.array() + .min(1), + + min: joi.number(), + + max: joi.number() + }) + .oxor('values', 'min') + .oxor('values', 'max') + .required() + }) + ) + }; + + static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated) + if (param === 'new') { + return joi.object({ + name: this.template.name.required(), + parameters: this.template.parameters.required() + }).validate(data); + } + else if (param === 'change') { + return joi.object({ + name: this.template.name, + parameters: this.template.parameters + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } + + static output (data) { // validate output from database for needed properties, strip everything else + data = IdValidate.stringify(data); + const {value, error} = joi.object({ + _id: IdValidate.get(), + name: this.template.name, + parameters: this.template.parameters + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} \ No newline at end of file diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts new file mode 100644 index 0000000..150bf64 --- /dev/null +++ b/src/routes/validate/user.ts @@ -0,0 +1,87 @@ +import joi from '@hapi/joi'; +import globals from '../../globals'; + +import IdValidate from './id'; + +export default class UserValidate { // validate input for user + private static user = { + name: joi.string() + .alphanum() + .lowercase() + .max(128), + + email: joi.string() + .email({minDomainSegments: 2}) + .lowercase() + .max(128), + + pass: joi.string() + .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')) + .max(128), + + level: joi.string() + .valid(...globals.levels), + + location: joi.string() + .alphanum() + .max(128), + + device_name: joi.string() + .allow('') + .max(128), + }; + + private static specialUsernames = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take + + static input (data, param) { + if (param === 'new') { + return joi.object({ + name: this.user.name.required(), + email: this.user.email.required(), + pass: this.user.pass.required(), + level: this.user.level.required(), + location: this.user.location.required(), + device_name: this.user.device_name.required() + }).validate(data); + } + else if (param === 'change') { + return joi.object({ + name: this.user.name, + email: this.user.email, + pass: this.user.pass, + location: this.user.location, + device_name: this.user.device_name + }).validate(data); + } + else if (param === 'changeadmin') { + return joi.object({ + name: this.user.name, + email: this.user.email, + pass: this.user.pass, + level: this.user.level, + location: this.user.location, + device_name: this.user.device_name + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } + + static output (data) { // validate output from database for needed properties, strip everything else + data = IdValidate.stringify(data); + const {value, error} = joi.object({ + _id: IdValidate.get(), + name: this.user.name, + email: this.user.email, + level: this.user.level, + location: this.user.location, + device_name: this.user.device_name + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } + + static isSpecialName (name) { // true if name belongs to special names + return this.specialUsernames.indexOf(name) > -1; + } +} diff --git a/src/test/db.json b/src/test/db.json new file mode 100644 index 0000000..2d8a7d0 --- /dev/null +++ b/src/test/db.json @@ -0,0 +1,281 @@ +{ + "collections": { + "samples": [ + { + "_id": {"$oid":"400000000000000000000001"}, + "number": "1", + "type": "granulate", + "color": "black", + "batch": "", + "validated": true, + "material_id": {"$oid":"100000000000000000000004"}, + "note_id": null, + "user_id": {"$oid":"000000000000000000000002"}, + "__v": 0 + }, + { + "_id": {"$oid":"400000000000000000000002"}, + "number": "21", + "type": "granulate", + "color": "natural", + "batch": "1560237365", + "validated": true, + "material_id": {"$oid":"100000000000000000000001"}, + "note_id": {"$oid":"500000000000000000000001"}, + "user_id": {"$oid":"000000000000000000000002"}, + "__v": 0 + }, + { + "_id": {"$oid":"400000000000000000000003"}, + "number": "33", + "type": "part", + "color": "black", + "batch": "1704-005", + "validated": false, + "material_id": {"$oid":"100000000000000000000005"}, + "note_id": {"$oid":"500000000000000000000002"}, + "user_id": {"$oid":"000000000000000000000003"}, + "__v": 0 + }, + { + "_id": {"$oid":"400000000000000000000004"}, + "number": "32", + "type": "granulate", + "color": "black", + "batch": "1653000308", + "validated": false, + "material_id": {"$oid":"100000000000000000000005"}, + "note_id": {"$oid":"500000000000000000000003"}, + "user_id": {"$oid":"000000000000000000000003"}, + "__v": 0 + } + ], + "notes": [ + { + "_id": {"$oid":"500000000000000000000001"}, + "comment": "Stoff gesperrt", + "sample_references": [], + "__v": 0 + }, + { + "_id": {"$oid":"500000000000000000000002"}, + "comment": "", + "sample_references": [{ + "id": "400000000000000000000004", + "relation": "granulate to sample" + }], + "custom_fields": { + "not allowed for new applications": true + }, + "__v": 0 + }, + { + "_id": {"$oid":"500000000000000000000003"}, + "comment": "", + "sample_references": [{ + "id": "400000000000000000000003", + "relation": "part to sample" + }], + "custom_fields": { + "not allowed for new applications": true + }, + "__v": 0 + } + ], + "note_fields": [ + { + "_id": {"$oid":"600000000000000000000001"}, + "name": "not allowed for new applications", + "qty": 2, + "__v": 0 + } + ], + "materials": [ + { + "_id": {"$oid":"100000000000000000000001"}, + "name": "Stanyl TW 200 F8", + "supplier": "DSM", + "group": "PA46", + "mineral": 0, + "glass_fiber": 40, + "carbon_fiber": 0, + "numbers": [ + { + "color": "black", + "number": 5514263423 + }, + { + "color": "natural", + "number": 5514263422 + } + ], + "__v": 0 + }, + { + "_id": {"$oid":"100000000000000000000002"}, + "name": "Ultramid T KR 4355 G7", + "supplier": "BASF", + "group": "PA6/6T", + "mineral": 0, + "glass_fiber": 35, + "carbon_fiber": 0, + "numbers": [ + { + "color": "black", + "number": 5514212901 + }, + { + "color": "signalviolet", + "number": 5514612901 + } + ], + "__v": 0 + }, + { + "_id": {"$oid":"100000000000000000000003"}, + "name": "PA GF 50 black (2706)", + "supplier": "Akro-Plastic", + "group": "PA66+PA6I/6T", + "mineral": 0, + "glass_fiber": 0, + "carbon_fiber": 0, + "numbers": [ + ], + "__v": 0 + }, + { + "_id": {"$oid":"100000000000000000000004"}, + "name": "Schulamid 66 GF 25 H", + "supplier": "Schulmann", + "group": "PA66", + "mineral": 0, + "glass_fiber": 25, + "carbon_fiber": 0, + "numbers": [ + { + "color": "black", + "number": 5513933405 + } + ], + "__v": 0 + }, + { + "_id": {"$oid":"100000000000000000000005"}, + "name": "Amodel A 1133 HS", + "supplier": "Solvay", + "group": "PPA", + "mineral": 0, + "glass_fiber": 33, + "carbon_fiber": 0, + "numbers": [ + { + "color": "black", + "number": 5514262406 + } + ], + "__v": 0 + } + ], + "treatment_templates": [ + { + "_id": {"$oid":"200000000000000000000001"}, + "name": "heat treatment", + "parameters": [ + { + "name": "material", + "range": { + "values": [ + "copper", + "hot air" + ] + } + }, + { + "name": "weeks", + "range": { + "min": 1, + "max": 10 + } + } + ] + }, + { + "_id": {"$oid":"200000000000000000000002"}, + "name": "heat treatment 2", + "parameters": [ + { + "name": "material", + "range": {} + } + ] + } + ], + "measurement_templates": [ + { + "_id": {"$oid":"300000000000000000000001"}, + "name": "spectrum", + "parameters": [ + { + "name": "dpt", + "range": {} + } + ] + }, + { + "_id": {"$oid":"300000000000000000000002"}, + "name": "kf", + "parameters": [ + { + "name": "weight %", + "range": { + "min": 0, + "max": 1.5 + } + }, + { + "name": "standard deviation", + "range": { + "min": 0, + "max": 0.5 + } + } + ] + } + ], + "users": [ + { + "_id": {"$oid":"000000000000000000000001"}, + "email": "user@bosch.com", + "name": "user", + "pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi", + "level": "read", + "location": "Rng", + "device_name": "Alpha I", + "key": "000000000000000000001001", + "__v": 0 + }, + { + "_id": {"$oid":"000000000000000000000002"}, + "email": "jane.doe@bosch.com", + "name": "janedoe", + "pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi", + "level": "write", + "location": "Rng", + "device_name": "Alpha I", + "key": "000000000000000000001002", + "__v": 0 + }, + { + "_id": {"$oid":"000000000000000000000003"}, + "email": "a.d.m.i.n@bosch.com", + "name": "admin", + "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K", + "level": "admin", + "location": "Rng", + "device_name": "", + "key": "000000000000000000001003", + "__v": "0" + } + ] + } +} \ No newline at end of file diff --git a/static/img/bosch-logo.svg b/static/img/bosch-logo.svg new file mode 100644 index 0000000..fae963f --- /dev/null +++ b/static/img/bosch-logo.svg @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/styles/swagger.css b/static/styles/swagger.css new file mode 100644 index 0000000..33bebe1 --- /dev/null +++ b/static/styles/swagger.css @@ -0,0 +1,306 @@ +/*Bosch styling for swagger*/ + +/*GET: dark blue*/ +/*POST: dark green*/ +/*PUT: turquoise*/ +/*DELETE: fuchsia*/ + +:root { + --red: #ea0016; + --dark-blue: #005691; + --dark-blue-w75: #bfd5e3; + --dark-green: #006249; + --dark-green-w75: #bfd8d1; + --turquoise: #00a8b0; + --turquoise-w75: #bfe9eb; + --fuchsia: #b90276; + --fuchsia-w75: #edc0dd; + --light-grey: #bfc0c2; + --light-grey-w75: #efeff0; + --light-green: #78be20; +} + +body { + background: #fff; +} + +body:before { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 16px; + content: ''; + background-repeat: no-repeat; + background-size: cover; + background-image: url(data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB4bWw6c3BhY2U9InByZXNlcnZlIgogIGhlaWdodD0iMzAwIgogIHdpZHRoPSI3MjAiCiAgdmVyc2lvbj0iMS4xIgogIHk9IjAiCiAgeD0iMCIKICB2aWV3Qm94PSIwIDAgNzIwIDMwMCI+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgIC5zdDAgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF8xXyIpOwogICAgfQogICAgLnN0MSB7CiAgICAgIGZpbGw6IHVybCgiI1NWR0lEXzJfIik7CiAgICB9CiAgICAuc3QyIHsKICAgICAgZmlsbDogdXJsKCIjU1ZHSURfM18iKTsKICAgIH0KICAgIC5zdDMgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF80XyIpOwogICAgfQogICAgLnN0NCB7CiAgICAgIGZpbGw6IHVybCgiI1NWR0lEXzVfIik7CiAgICB9CiAgICAuc3Q1IHsKICAgICAgZmlsbDogI0FGMjAyNDsKICAgIH0KICAgIC5zdDYgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF82XyIpOwogICAgfQogICAgLnN0NyB7CiAgICAgIGZpbGw6ICM5NDFCMUU7CiAgICB9CiAgICAuc3Q4IHsKICAgICAgZmlsbDogI0IxMjczOTsKICAgIH0KICAgIC5zdDkgewogICAgICBmaWxsOiAjOTUyNDMyOwogICAgfQogICAgLnN0MTAgewogICAgICBmaWxsOiAjRDQyMDI3OwogICAgfQogICAgLnN0MTEgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF83XyIpOwogICAgfQogICAgLnN0MTIgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF84XyIpOwogICAgfQogICAgLnN0MTMgewogICAgICBmaWxsOiAjMUM5QTQ4OwogICAgfQogICAgLnN0MTQgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF85XyIpOwogICAgfQogICAgLnN0MTUgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF8xMF8iKTsKICAgIH0KICAgIC5zdDE2IHsKICAgICAgZmlsbDogIzJBMzg4NjsKICAgIH0KICAgIC5zdDE3IHsKICAgICAgZmlsbDogdXJsKCIjU1ZHSURfMTFfIik7CiAgICB9CiAgICAuc3QxOCB7CiAgICAgIGZpbGw6IHVybCgiI1NWR0lEXzEyXyIpOwogICAgfQogICAgLnN0MTkgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF8xM18iKTsKICAgIH0KICAgIC5zdDIwIHsKICAgICAgZmlsbDogdXJsKCIjU1ZHSURfMTRfIik7CiAgICB9CiAgPC9zdHlsZT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMS41NSwtMy4zKSI+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzFfIiB5Mj0iLTMyLjY2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHkxPSItMzIuNjYzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9Ijg0Mi4wOCIgeDE9IjExOC45OCI+PHN0b3Agc3RvcC1jb2xvcj0iIzk1MjMzMSIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzkyMUMxRCIgb2Zmc2V0PSIuMDM2MDk0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iI0IwMjczOSIgb2Zmc2V0PSIuMDg0NjQ5Ii8+PHN0b3Agc3RvcC1jb2xvcj0iI0FEMUYyNCIgb2Zmc2V0PSIuMTIzNyIvPjxzdG9wIHN0b3AtY29sb3I9IiNDNzIwMjYiIG9mZnNldD0iLjE1MDkiLz48c3RvcCBzdG9wLWNvbG9yPSIjRDQyMDI3IiBvZmZzZXQ9Ii4xNjk3Ii8+PHN0b3Agc3RvcC1jb2xvcj0iI0NDMjQzMSIgb2Zmc2V0PSIuMTc1OCIvPjxzdG9wIHN0b3AtY29sb3I9IiNCNzJCNEMiIG9mZnNldD0iLjE4ODgiLz48c3RvcCBzdG9wLWNvbG9yPSIjOTUzMzcxIiBvZmZzZXQ9Ii4yMDc0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzg4MzU3RiIgb2Zmc2V0PSIuMjE0MiIvPjxzdG9wIHN0b3AtY29sb3I9IiM4NTM2ODEiIG9mZnNldD0iLjI0MzYiLz48c3RvcCBzdG9wLWNvbG9yPSIjNkYzNjhCIiBvZmZzZXQ9Ii4yNjM4Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzM5NDI4RiIgb2Zmc2V0PSIuMjkxMSIvPjxzdG9wIHN0b3AtY29sb3I9IiMyMzNEN0QiIG9mZnNldD0iLjMyNDIiLz48c3RvcCBzdG9wLWNvbG9yPSIjMzIyQzZGIiBvZmZzZXQ9Ii40MTgxIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzJBMzg4NSIgb2Zmc2V0PSIuNDk0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzFENjJBMSIgb2Zmc2V0PSIuNTU4MSIvPjxzdG9wIHN0b3AtY29sb3I9IiMyNzZDQTUiIG9mZnNldD0iLjU3MDIiLz48c3RvcCBzdG9wLWNvbG9yPSIjNDM4RUIzIiBvZmZzZXQ9Ii42MTAzIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzU1QTVCQyIgb2Zmc2V0PSIuNjM5OSIvPjxzdG9wIHN0b3AtY29sb3I9IiM1Q0FGQkYiIG9mZnNldD0iLjY1NTYiLz48c3RvcCBzdG9wLWNvbG9yPSIjNTZBQkJEIiBvZmZzZXQ9Ii42Nzc3Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzQzOUZCOCIgb2Zmc2V0PSIuNzA1OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMxODhFQUYiIG9mZnNldD0iLjczNzIiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDM4QkFFIiBvZmZzZXQ9Ii43NDI2Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzA2OTI5MiIgb2Zmc2V0PSIuNzg5OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMwNUExNEIiIG9mZnNldD0iLjg4NzUiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDM5MjdFIiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxyZWN0IHdpZHRoPSI3MjMuMSIgeT0iMCIgeD0iMCIgaGVpZ2h0PSIzMDYuNCIgY2xhc3M9InN0MCIgZmlsbD0idXJsKCNTVkdJRF8xXykiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iU1ZHSURfMl8iIHkyPSItMTA5LjI2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9Ii0xMDkuMjYiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgLTExOC45OCAxMjAuNTQpIiB4Mj0iMjM1Ljk4IiB4MT0iMzI1LjA4Ij48c3RvcCBzdG9wLWNvbG9yPSIjODkzNjgwIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjODkzNjgwIiBvZmZzZXQ9Ii4zMzU0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzhEMzE2RCIgb2Zmc2V0PSIuNTAyNSIvPjxzdG9wIHN0b3AtY29sb3I9IiM5MDI5NEQiIG9mZnNldD0iLjgzOTgiLz48c3RvcCBzdG9wLWNvbG9yPSIjOTAyNTQxIiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50Pjxwb2x5Z29uIHBvaW50cz0iMTc1LjEgMTUzLjIgMTE3IDMwNi40IDIwNi4xIDMwNi40IiBmaWxsPSJ1cmwoI1NWR0lEXzJfKSIgY2xhc3M9InN0MSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8zXyIgeTI9Ii04Mi4yODQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjI0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjQ0Ni41NSIgeDE9IjQ3OC45MyI+PHN0b3Agc3RvcC1jb2xvcj0iIzMyMkM2RiIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzMyMkM2RiIgb2Zmc2V0PSIuMjQyNyIvPjxzdG9wIHN0b3AtY29sb3I9IiMzMDJGNzIiIG9mZnNldD0iLjQ1OTkiLz48c3RvcCBzdG9wLWNvbG9yPSIjMkEzQTdFIiBvZmZzZXQ9Ii43MTU1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzE1NEE5MyIgb2Zmc2V0PSIuOTg5NiIvPjxzdG9wIHN0b3AtY29sb3I9IiMxMzRCOTQiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBvbHlnb24gcG9pbnRzPSIyODguNCAxNTMuMiAzMTAuNyAzMDYuNCAzNTguMSAzMDYuNCAzNTguMSAwIDMxMi45IDAiIGZpbGw9InVybCgjU1ZHSURfM18pIiBjbGFzcz0ic3QyIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzRfIiB5Mj0iLTMyLjY2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHkxPSItMzIuNjYzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjM3Mi44OCIgeDE9IjI5NC4wOCI+PHN0b3Agc3RvcC1jb2xvcj0iIzZGMzc4RCIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzNBNDI5MSIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cG9seWdvbiBwb2ludHM9IjE3NS4xIDE1My4yIDIwNi4xIDMwNi40IDI1My45IDE1My4yIDIwOS40IDAgMjA5LjQgMCIgZmlsbD0idXJsKCNTVkdJRF80XykiIGNsYXNzPSJzdDMiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iU1ZHSURfNV8iIHkyPSItMzIuNjYzIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9Ii0zMi42NjMiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgLTExOC45OCAxMjAuNTQpIiB4Mj0iMzI1LjA4IiB4MT0iNDMxLjg4Ij48c3RvcCBzdG9wLWNvbG9yPSIjMjMzRDdEIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjMjkzRDdEIiBvZmZzZXQ9Ii4yNDk1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzNBM0M4MCIgb2Zmc2V0PSIuNTQ0NiIvPjxzdG9wIHN0b3AtY29sb3I9IiM1MTNCODQiIG9mZnNldD0iLjg2MTYiLz48c3RvcCBzdG9wLWNvbG9yPSIjNUQzQTg2IiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50Pjxwb2x5Z29uIHBvaW50cz0iMjUzLjkgMTUzLjIgMjA2LjEgMzA2LjQgMzEwLjcgMzA2LjQgMjg4LjQgMTUzLjIgMzEyLjkgMCAyMDkuNCAwIiBmaWxsPSJ1cmwoI1NWR0lEXzVfKSIgY2xhc3M9InN0NCIvPjxwb2x5Z29uIHBvaW50cz0iMTE2LjEgMCA1NS43IDAgNTUuNyA5NC44IDg5LjkgMTUzLjIgNTUuNyAyMTEuNiA1NS43IDMwNi40IDExNyAzMDYuNCA5NS4yIDE1My4yIiBmaWxsPSIjYWYyMDI0IiBjbGFzcz0ic3Q1Ii8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzZfIiB5Mj0iNDMuOTM3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9IjQzLjkzNyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMTE4Ljk4IDEyMC41NCkiIHgyPSIyMzIuNjciIHgxPSIzMjkuMTEiPjxzdG9wIHN0b3AtY29sb3I9IiM4OTM2ODAiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiM4OTM2ODAiIG9mZnNldD0iLjMzNTQiLz48c3RvcCBzdG9wLWNvbG9yPSIjOEQzMTZEIiBvZmZzZXQ9Ii41MDI1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzkwMjk0RCIgb2Zmc2V0PSIuODM5OCIvPjxzdG9wIHN0b3AtY29sb3I9IiM5MDI1NDEiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBvbHlnb24gcG9pbnRzPSIxNzUuMSAxNTMuMiAyMDkuNCAwIDExNi4xIDAiIGZpbGw9InVybCgjU1ZHSURfNl8pIiBjbGFzcz0ic3Q2Ii8+PHBvbHlnb24gcG9pbnRzPSI1NS43IDk0LjggNTUuNyAwIDAgMCIgZmlsbD0iIzk0MWIxZSIgY2xhc3M9InN0NyIvPjxwb2x5Z29uIHBvaW50cz0iNTUuNyAyMTEuNiA4OS45IDE1My4yIDU1LjcgOTQuOCIgZmlsbD0iI2IxMjczOSIgY2xhc3M9InN0OCIvPjxwb2x5Z29uIHBvaW50cz0iNTUuNyAyMTEuNiAwIDMwNi40IDU1LjcgMzA2LjQiIGZpbGw9IiM5NDFiMWUiIGNsYXNzPSJzdDciLz48cG9seWdvbiBwb2ludHM9IjU1LjcgOTQuOCAwIDAgMCAzMDYuNCA1NS43IDIxMS42IiBmaWxsPSIjOTUyNDMyIiBjbGFzcz0ic3Q5Ii8+PHBvbHlnb24gcG9pbnRzPSIxMTYuMSAwIDk1LjIgMTUzLjIgMTE3IDMwNi40IDE3NS4xIDE1My4yIiBmaWxsPSIjZDQyMDI3IiBjbGFzcz0ic3QxMCIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF83XyIgeTI9Ii0xODYuMDYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjQ0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9Ijc0OC45NiIgeDE9Ijc0OC45NiI+PHN0b3Agc3RvcC1jb2xvcj0iIzk0QkU1NSIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzkzQkQ1OCIgb2Zmc2V0PSIuMDQ0MzQwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzhCQkM2QSIgb2Zmc2V0PSIuMzg5MSIvPjxzdG9wIHN0b3AtY29sb3I9IiM4NkJDNzUiIG9mZnNldD0iLjcxNDkiLz48c3RvcCBzdG9wLWNvbG9yPSIjODRCQzc5IiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxwYXRoCiAgICAgIGQ9Im02NDEuNiAyNTkuNmMxLjctMjUuNCAxMC01NC42IDE4LjgtODUuNiAxLjQtNSAyLjgtMTAgNC4yLTE1LjEtMS40LTUuNS0yLjgtMTAuOS00LjItMTYuMi04LjgtMzMuMy0xNy02NC43LTE4LjgtOTItMS40LTIxLjIgMS40LTM3IDguOS01MC42aC00NS45Yy03LjUgMTguMy0xMC4zIDI5LjEtOC45IDUwLjMgMS43IDI3LjMgMTAgNTguNyAxOC44IDkyIDEzIDQ5LjMgMjggMTA2LjIgMjMuMiAxNjQuMmgxMi45Yy03LjYtMTIuOC0xMC40LTI3LjMtOS00N3oiCiAgICAgIGNsYXNzPSJzdDExIgogICAgICBmaWxsPSJ1cmwoI1NWR0lEXzdfKSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF84XyIgeTI9Ii0xODQuNDUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTE3LjI5IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjczMy40OSIgeDE9IjY1My43NiI+PHN0b3Agc3RvcC1jb2xvcj0iIzA4QTI0QiIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzBBQTE0RSIgb2Zmc2V0PSIuMTY3OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMwQjlFNTciIG9mZnNldD0iLjQwNDciLz48c3RvcCBzdG9wLWNvbG9yPSIjMDk5QTY3IiBvZmZzZXQ9Ii42ODI3Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzA0OTQ3RCIgb2Zmc2V0PSIuOTg5OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMwNDkzN0UiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0ibTYxNC41IDE0Mi4zYy04LjgtMzMuMy0xNy02NC43LTE4LjgtOTItMS40LTIxLjIgMS40LTMyIDguOS01MC4zaC0zNS40YzUuNyA1My45LTMuOCAxMDYuNy0xMy42IDE2Ni44LTUuNyAzNS0xMS43IDcxLjMtMTMuMiAxMDAuNi0xLjEgMjEuMSAwLjQgMzIuOCAxLjggMzloOTMuNWM0LjgtNTcuOS0xMC4zLTExNC44LTIzLjItMTY0LjF6IiBjbGFzcz0ic3QxMiIgZmlsbD0idXJsKCNTVkdJRF84XykiLz48cGF0aCBjbGFzcz0ic3QxMyIgZmlsbD0iIzFjOWE0OCIgZD0ibTY2NC42IDE1OC45Yy0xLjQgNS4xLTIuOCAxMC4xLTQuMiAxNS4xLTguOCAzMS0xNyA2MC4yLTE4LjggODUuNi0xLjQgMTkuNyAxLjQgMzQuMiA5IDQ2LjloMzNjNC4yLTUxLjgtNy4yLTEwMi4zLTE5LTE0Ny42eiIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF85XyIgeTI9Ii0xODUuOTYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjU0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjgxMi44MyIgeDE9IjgxMi44MyI+PHN0b3Agc3RvcC1jb2xvcj0iIzY5QTA2MCIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzYzOUQ1QyIgb2Zmc2V0PSIuMDM5ODk1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzRDOTQ0RiIgb2Zmc2V0PSIuMjE5MiIvPjxzdG9wIHN0b3AtY29sb3I9IiMzNzhFNDciIG9mZnNldD0iLjQxODQiLz48c3RvcCBzdG9wLWNvbG9yPSIjMjk4QjQ0IiBvZmZzZXQ9Ii42NTE1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzIzOEE0MyIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJtNjgwLjUgMGMxMC43IDU1LjMtMi41IDExMC40LTE1LjkgMTU4LjkgMTEuNyA0NS4zIDIzLjIgOTUuOCAxOC45IDE0Ny42aDM5LjZ2LTMwNi41aC00Mi42eiIgY2xhc3M9InN0MTQiIGZpbGw9InVybCgjU1ZHSURfOV8pIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzEwXyIgeTI9Ii0xODUuODYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjU0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjY1Mi40NSIgeDE9IjY1Mi40NSI+PHN0b3Agc3RvcC1jb2xvcj0iIzA1QjVEQyIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzA0QjBENyIgb2Zmc2V0PSIuMjE5NyIvPjxzdG9wIHN0b3AtY29sb3I9IiMwNUE0QzkiIG9mZnNldD0iLjUzNzEiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDU5MUI0IiBvZmZzZXQ9Ii45MTIyIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzA1OENBRSIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJtNTQyLjMgMjY3LjRjMS41LTI5LjQgNy41LTY1LjYgMTMuMi0xMDAuNiA5LjgtNjAuMSAxOS4zLTExMi44IDEzLjYtMTY2LjhoLTcwLjhjLTEuNCAxMS40LTIuOSAxOS4yLTEuOCA0MS44IDEuNSAzMS42IDcuNSA3MC41IDEzLjIgMTA4LjIgOC40IDU1LjQgMTYuNiAxMDguOCAxNS4xIDE1Ni40aDE5LjJjLTEuMy02LjItMi44LTE3LjktMS43LTM5eiIgY2xhc3M9InN0MTUiIGZpbGw9InVybCgjU1ZHSURfMTBfKSIvPjxwb2x5Z29uIHBvaW50cz0iMzc1LjcgMTUzLjIgMzU4LjEgMCAzNTguMSAzMDYuNCIgZmlsbD0iIzJhMzg4NiIgY2xhc3M9InN0MTYiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iU1ZHSURfMTFfIiB5Mj0iNzcuMTM2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9Ii00LjMyODEiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgLTExOC45OCAxMjAuNTQpIiB4Mj0iNzk2LjcxIiB4MT0iNzUxLjA1Ij48c3RvcCBzdG9wLWNvbG9yPSIjNjJCMTZFIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjODdCOTU3IiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxwYXRoIGQ9Im02NDEuNiA1MC42YzEuNyAyNy4zIDEwIDU4LjcgMTguOCA5MiAxLjQgNS4zIDIuOCAxMC43IDQuMiAxNi4yIDEzLjUtNDguNCAyNi42LTEwMy41IDE1LjktMTU4LjhoLTMwYy03LjUgMTMuNi0xMC4zIDI5LjQtOC45IDUwLjZ6IiBjbGFzcz0ic3QxNyIgZmlsbD0idXJsKCNTVkdJRF8xMV8pIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzEyXyIgeTI9Ii0xODkuMjgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTEzLjcxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjYzMS41OSIgeDE9IjU1MC40Ij48c3RvcCBzdG9wLWNvbG9yPSIjMDY5QUQ0IiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjMzBBMENFIiBvZmZzZXQ9Ii4zNTI1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzVCQjBDMCIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJtNTA5LjggMTUwYy01LjctMzcuNy0xMS43LTc2LjYtMTMuMi0xMDguMi0xLjEtMjIuNyAwLjQtMzAuNCAxLjgtNDEuOGgtNDEuNWMxLjUgNDAuMS0xLjUgODUuMy03IDE2MC44LTMuMSA0My41LTggMTEwLjUtNyAxNDUuN2g4Mi4xYzEuNC00Ny43LTYuOC0xMDEuMS0xNS4yLTE1Ni41eiIgY2xhc3M9InN0MTgiIGZpbGw9InVybCgjU1ZHSURfMTJfKSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8xM18iIHkyPSItMTg1Ljg2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9IjEyMC41NCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMTE4Ljk4IDEyMC41NCkiIHgyPSI1MDUuMzMiIHgxPSI1MDUuMzMiPjxzdG9wIHN0b3AtY29sb3I9IiMxRTQ1OEUiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiMxRjRGOTYiIG9mZnNldD0iLjI0MTEiLz48c3RvcCBzdG9wLWNvbG9yPSIjMkI2QUFCIiBvZmZzZXQ9Ii43MjkyIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzMzN0JCOSIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cG9seWdvbiBwb2ludHM9IjM1OC4xIDMwNi40IDQxNC42IDMwNi40IDQxNC42IDAgMzU4LjEgMCAzNzUuNyAxNTMuMiIgZmlsbD0idXJsKCNTVkdJRF8xM18pIiBjbGFzcz0ic3QxOSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8xNF8iIHkyPSIxMjAuNTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iLTE4NS44NiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMTE4Ljk4IDEyMC41NCkiIHgyPSI1NTQuOTIiIHgxPSI1NTQuOTIiPjxzdG9wIHN0b3AtY29sb3I9IiMzRjlBQzkiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiMyMDYyQTIiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0ibTQ0OS45IDE2MC44YzUuNS03NS41IDguNS0xMjAuNiA3LTE2MC44aC00Mi4ybC0wLjEgMzA2LjRoMjguM2MtMS0zNS4xIDMuOC0xMDIuMSA3LTE0NS42eiIgY2xhc3M9InN0MjAiIGZpbGw9InVybCgjU1ZHSURfMTRfKSIvPjwvZz4KPC9zdmc+Cg==); +} + +body:after { + position: absolute; + right: 25px; + top: 36px; + width: 135px; + height: 48px; + content: ''; + background-repeat: no-repeat; + background-size: cover; + background-image: url(/static/img/bosch-logo.svg); +} + +.swagger-ui { + font-family: "Bosch Sans", sans-serif; +} + +/*Remove topbar*/ +.swagger-ui .topbar { + display: none +} + +/*Remove models view*/ +.swagger-ui .models { + display: none; +} + +/*Remove application/json select*/ +.swagger-ui .opblock .opblock-section-header > label, .swagger-ui .response-controls { + display: none; +} + +/*Remove border radius*/ +.swagger-ui .opblock, .swagger-ui .opblock .opblock-summary-method, .swagger-ui select { + border-radius: 0; + box-shadow: none; +} + +/*remove links in response*/ +.swagger-ui .response-col_links { + display: none; +} + +/*remove version*/ +.swagger-ui .info .title span { + display: none; +} + +/*separator before methods*/ +.swagger-ui .scheme-container { + box-shadow: none; + border-bottom: 1px solid var(--light-grey); +} + +/*tag separator*/ +.swagger-ui .opblock-tag { + border-bottom: 1px solid var(--light-grey); +} + +/*parameters/responses bar*/ +.swagger-ui .opblock .opblock-section-header { + box-shadow: none; + background: #fff; +} + +/*select*/ +.swagger-ui select { + background-color: var(--light-grey-w75); + border: none; + height: 36px; +} + +/*button*/ +.swagger-ui .btn { + border-radius: 0; + box-shadow: none; +} + +.swagger-ui .btn:hover { + box-shadow: none; +} + +/*authorize button */ +.swagger-ui .btn.authorize { + color: var(--light-green); + border-color: var(--light-green); +} + +.swagger-ui .btn.authorize svg { + fill: var(--light-green); +} + +/*auth inputs*/ +.swagger-ui .auth-container input[type="password"], .swagger-ui .auth-container input[type="text"] { + border-radius: 0; + box-shadow: none; + border-color: var(--light-grey); +} + +.swagger-ui .dialog-ux .modal-ux { + border-radius: 0; +} + +/*cancel button*/ +.swagger-ui .btn.cancel { + color: var(--red); + border-color: var(--red); +} + +/*download button*/ +.swagger-ui .download-contents { + border-radius: 0; + height: 28px; + width: 80px; +} + +/*model*/ +.swagger-ui .model-box { + border-radius: 0; +} + +/*execute button*/ +.swagger-ui .btn.execute { + background-color: var(--dark-blue); + border-color: var(--dark-blue); + height: 30px; + line-height: 0.7; +} + +.swagger-ui .btn-group .btn:last-child { + border-radius: 0; + height: 30px; + border-color: var(--dark-blue); +} + +.swagger-ui .btn-group .btn:first-child { + border-radius: 0; +} + +.swagger-ui .btn-group { + padding: 0 20px; +} + +/*parameter input*/ +.swagger-ui .parameters-col_description input[type="text"] { + border-radius: 0; +} + +/*required label*/ +.swagger-ui .parameter__name.required > span { + color: var(--red) !important; +} + +.swagger-ui .parameter__name.required::after { + color: var(--red); +} +/*Remove colored parameters bar*/ +.swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after { + background: none; +} + +/*code*/ +.swagger-ui .opblock-body pre.microlight { + border-radius: 0; +} + +.swagger-ui .highlight-code > .microlight { + min-height: 0; +} + +/*request body*/ +.swagger-ui textarea { + border-radius: 0; +} + +/*parameters smaller padding*/ +.swagger-ui .execute-wrapper { + padding-top: 0; + padding-bottom: 0; +} + +.swagger-ui .btn.execute { + margin-bottom: 20px; +} + +.swagger-ui .opblock-description-wrapper { + margin-top: 20px; +} + +.swagger-ui .opblock-description-wrapper { + margin-top: 5px; +} + +.opblock-section .opblock-section-request-body > div > div { + padding-top: 18px; +} + +/*response element positions*/ +.swagger-ui .model-example { + position: relative; + margin-top: 0; +} + +.swagger-ui .tab { + position: absolute; + top: -35px; + right: 0; +} + +.swagger-ui table tbody tr td { + padding: 0; +} + +.swagger-ui .renderedMarkdown p { + margin: 8px auto; +} + +/*Method colors*/ +.swagger-ui .opblock.opblock-get .opblock-summary-method { + background: var(--dark-blue); +} + +.swagger-ui .opblock.opblock-get .opblock-summary { + border-color: var(--dark-blue); +} + +.swagger-ui .opblock.opblock-get { + background: var(--dark-blue-w75); + border-color: var(--dark-blue); +} + +.swagger-ui .opblock.opblock-post .opblock-summary-method { + background: var(--dark-green); +} + +.swagger-ui .opblock.opblock-post .opblock-summary { + border-color: var(--dark-green); +} + +.swagger-ui .opblock.opblock-post { + background: var(--dark-green-w75); + border-color: var(--dark-green); +} + +.swagger-ui .opblock.opblock-put .opblock-summary-method { + background: var(--turquoise); +} + +.swagger-ui .opblock.opblock-put .opblock-summary { + border-color: var(--turquoise); +} + +.swagger-ui .opblock.opblock-put { + background: var(--turquoise-w75); + border-color: var(--turquoise); +} + +.swagger-ui .opblock.opblock-delete .opblock-summary-method { + background: var(--fuchsia); +} + +.swagger-ui .opblock.opblock-delete .opblock-summary { + border-color: var(--fuchsia); +} + +.swagger-ui .opblock.opblock-delete { + background: var(--fuchsia-w75); + border-color: var(--fuchsia); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index c49a622..b43a5fb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,13 +4,21 @@ "target": "es5", "outDir": "dist", "sourceMap": true, - "esModuleInterop": true + "esModuleInterop": true, + "resolveJsonModule": true, + "incremental": true, + "diagnostics": true, + "typeRoots": [ + "src/customTypings", + "node_modules/@types" + ] }, "files": [ "./node_modules/@types/node/index.d.ts" ], "include": [ - "src/**/*.ts" + "src/**/*.ts", + "src/**/*.json" ], "exclude": [ "node_modules"