Archived
2

Merge pull request #2 in ~VLE2FE/dfop-api from user to develop

* commit '20f57acd2aa031a3fbce7b4f61f6a64749d98606':
  implemented first /sample methods
  finished /template methods
  finished /material methods
  styled swagger
  added /materials route
  added custom type definitions
  added /user DELETE route
  added /user/key and edited /user regex
  added test helper and rewrote tests
  added PUT /user route
  added GET /user route
  changed to findById and improved db.loadJson
  added passreset and mail helper
  added authorization
  cannot add username twice
  implemented first tests and basic functionality
This commit is contained in:
Veit Lukas (PEA4-Fe) 2020-05-06 14:41:09 +02:00
commit c407da2fbc
61 changed files with 5773 additions and 1082 deletions

1
.gitignore vendored
View File

@ -112,3 +112,4 @@ dist
**/.idea/tasks.xml **/.idea/tasks.xml
**/.idea/shelf **/.idea/shelf
**/.idea/*.iml **/.idea/*.iml
/tmp/

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -0,0 +1,9 @@
<component name="ProjectDictionaryState">
<dictionary name="VLE2FE">
<words>
<w>bcrypt</w>
<w>cfenv</w>
<w>dfopdb</w>
</words>
</dictionary>
</component>

View File

@ -2,5 +2,6 @@
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ReservedWordUsedAsNameJS" enabled="false" level="WARNING" enabled_by_default="false" />
</profile> </profile>
</component> </component>

View File

@ -6,7 +6,10 @@ info:
version: 1.0.0 version: 1.0.0
description: | description: |
This API gives access to the project database.<br> This API gives access to the project database.<br>
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<br> 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<br>
The description lists available authentication methods, also the locks of each method close correspondingly
if the entered authentication is allowed.<br><br>
There are a number of different user levels: <br> There are a number of different user levels: <br>
<ul> <ul>
<li>read: read access to the samples database</li> <li>read: read access to the samples database</li>
@ -15,6 +18,15 @@ info:
<li>dev: handling machine learning models</li> <li>dev: handling machine learning models</li>
<li>admin: user administration</li> <li>admin: user administration</li>
</ul> </ul>
Password policy:
<ul>
<li>at least one digit</li>
<li>at least one lower case letter</li>
<li>at least one upper case letter</li>
<li>at least one of the following special characters: !"#%&'()*+,-./:;<=>?@[\]^_`{|}~</li>
<li>no whitespace</li>
<li>at least 8 characters</li>
</ul>
@ -36,7 +48,7 @@ tags:
- name: /material - name: /material
- name: /condition - name: /condition
- name: /measurement - name: /measurement
- name: /templates - name: /template
- name: /model - name: /model
- name: /user - name: /user

73
api/condition.yaml Normal file
View File

@ -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'

119
api/material.yaml Normal file
View File

@ -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'

73
api/measurement.yaml Normal file
View File

@ -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'

70
api/model.yaml Normal file
View File

@ -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'

43
api/others.yaml Normal file
View File

@ -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'

View File

@ -4,8 +4,10 @@ Id:
required: true required: true
schema: schema:
type: string type: string
example: 5ea0450ed851c30a90e70894
Name: Name:
name: name name: name
description: has to be URL encoded
in: path in: path
required: true required: true
schema: schema:

145
api/sample.yaml Normal file
View File

@ -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'

183
api/schemas.yaml Normal file
View File

@ -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

263
api/template.yaml Normal file
View File

@ -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'

255
api/user.yaml Normal file
View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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'

View File

@ -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'

284
package-lock.json generated
View File

@ -32,6 +32,49 @@
"js-tokens": "^4.0.0" "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": { "@jsdevtools/ono": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz",
@ -50,21 +93,102 @@
"defer-to-connect": "^1.0.1" "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": { "@types/color-name": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" "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": { "@types/mocha": {
"version": "5.2.7", "version": "5.2.7",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
"integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==" "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": { "@types/node": {
"version": "13.1.6", "version": "13.1.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.6.tgz",
"integrity": "sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg==" "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": { "abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -160,11 +284,32 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true "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": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" "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": { "binary-extensions": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
@ -514,6 +659,11 @@
"safe-buffer": "5.1.2" "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": { "content-type": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
@ -645,9 +795,9 @@
} }
}, },
"es-abstract": { "es-abstract": {
"version": "1.17.0", "version": "1.17.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
"integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
"dev": true, "dev": true,
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
@ -787,6 +937,24 @@
"is-buffer": "~2.0.3" "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": { "form-data": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
@ -1181,12 +1349,12 @@
"dev": true "dev": true
}, },
"log-symbols": { "log-symbols": {
"version": "2.2.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
"integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "^2.0.1" "chalk": "^2.4.2"
} }
}, },
"lowercase-keys": { "lowercase-keys": {
@ -1275,9 +1443,9 @@
} }
}, },
"mocha": { "mocha": {
"version": "7.0.0", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-7.0.0.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz",
"integrity": "sha512-CirsOPbO3jU86YKjjMzFLcXIb5YiGLUrjrXFHoJ3e2z9vWiaZVCZQ2+gtRGMPWF+nFhN6AWwLM/juzAQ6KRkbA==", "integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==",
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-colors": "3.2.3", "ansi-colors": "3.2.3",
@ -1291,9 +1459,9 @@
"growl": "1.10.5", "growl": "1.10.5",
"he": "1.2.0", "he": "1.2.0",
"js-yaml": "3.13.1", "js-yaml": "3.13.1",
"log-symbols": "2.2.0", "log-symbols": "3.0.0",
"minimatch": "3.0.4", "minimatch": "3.0.4",
"mkdirp": "0.5.1", "mkdirp": "0.5.5",
"ms": "2.1.1", "ms": "2.1.1",
"node-environment-flags": "1.0.6", "node-environment-flags": "1.0.6",
"object.assign": "4.1.0", "object.assign": "4.1.0",
@ -1301,8 +1469,8 @@
"supports-color": "6.0.0", "supports-color": "6.0.0",
"which": "1.3.1", "which": "1.3.1",
"wide-align": "1.1.3", "wide-align": "1.1.3",
"yargs": "13.3.0", "yargs": "13.3.2",
"yargs-parser": "13.1.1", "yargs-parser": "13.1.2",
"yargs-unparser": "1.6.0" "yargs-unparser": "1.6.0"
}, },
"dependencies": { "dependencies": {
@ -1351,21 +1519,6 @@
"path-is-absolute": "^1.0.0" "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": { "ms": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "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": { "mongodb": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.4.1.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.4.1.tgz",
@ -1586,9 +1744,9 @@
"integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw=="
}, },
"p-limit": { "p-limit": {
"version": "2.2.2", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true, "dev": true,
"requires": { "requires": {
"p-try": "^2.0.0" "p-try": "^2.0.0"
@ -2009,24 +2167,46 @@
"strip-ansi": "^4.0.0" "strip-ansi": "^4.0.0"
} }
}, },
"string.prototype.trimleft": { "string.prototype.trimend": {
"version": "2.1.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
"integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
"dev": true, "dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "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": { "string.prototype.trimright": {
"version": "2.1.1", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz",
"integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==",
"dev": true, "dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "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": { "string_decoder": {
@ -2459,9 +2639,9 @@
"dev": true "dev": true
}, },
"yargs": { "yargs": {
"version": "13.3.0", "version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true, "dev": true,
"requires": { "requires": {
"cliui": "^5.0.0", "cliui": "^5.0.0",
@ -2473,7 +2653,7 @@
"string-width": "^3.0.0", "string-width": "^3.0.0",
"which-module": "^2.0.0", "which-module": "^2.0.0",
"y18n": "^4.0.0", "y18n": "^4.0.0",
"yargs-parser": "^13.1.1" "yargs-parser": "^13.1.2"
}, },
"dependencies": { "dependencies": {
"ansi-regex": { "ansi-regex": {
@ -2505,21 +2685,13 @@
} }
}, },
"yargs-parser": { "yargs-parser": {
"version": "13.1.1", "version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true, "dev": true,
"requires": { "requires": {
"camelcase": "^5.0.0", "camelcase": "^5.0.0",
"decamelize": "^1.2.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": { "yargs-unparser": {

View File

@ -4,8 +4,9 @@
"description": "API for the digital fingerprint of plastics mongodb", "description": "API for the digital fingerprint of plastics mongodb",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"tsc": "tsc",
"test": "mocha dist/**/**.spec.js", "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\"" "dev": "nodemon -e ts,yaml --exec \"npm run start\""
}, },
"keywords": [], "keywords": [],
@ -13,11 +14,24 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": "^8.0.0", "@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/mocha": "^5.2.7",
"@types/mongoose": "^5.7.12",
"@types/node": "^13.1.6", "@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", "cfenv": "^1.2.2",
"content-filter": "^1.1.2",
"express": "^4.17.1", "express": "^4.17.1",
"json-schema": "^0.2.5", "json-schema": "^0.2.5",
"mongo-sanitize": "^1.1.0",
"mongoose": "^5.8.7", "mongoose": "^5.8.7",
"nodemon": "^2.0.3", "nodemon": "^2.0.3",
"swagger-ui-express": "^4.1.2", "swagger-ui-express": "^4.1.2",
@ -25,7 +39,7 @@
"typescript": "^3.7.4" "typescript": "^3.7.4"
}, },
"devDependencies": { "devDependencies": {
"mocha": "^7.0.0", "mocha": "^7.1.2",
"should": "^13.2.3", "should": "^13.2.3",
"supertest": "^4.0.2" "supertest": "^4.0.2"
} }

116
src/customTypes/express.ts Normal file
View File

@ -0,0 +1,116 @@
// Type definitions for Express 4.17
// Project: http://expressjs.com
// Definitions by: Boris Yankov <https://github.com/borisyankov>
// China Medical University Hospital <https://github.com/CMUH>
// Puneet Arora <https://github.com/puneetar>
// Dylan Frankland <https://github.com/dfrankland>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
/* =================== USAGE ===================
import * as express from "express";
var app = express();
=============================================== */
/// <reference types="express-serve-static-core" />
/// <reference types="serve-static" />
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 childs 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<P extends core.Params = core.ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = core.Query>
extends core.ErrorRequestHandler<P, ResBody, ReqBody, ReqQuery> { }
interface Express extends core.Express { }
interface Handler extends core.Handler { }
interface IRoute extends core.IRoute { }
interface IRouter extends core.IRouter { }
interface IRouterHandler<T> extends core.IRouterHandler<T> { }
interface IRouterMatcher<T> extends core.IRouterMatcher<T> { }
interface MediaType extends core.MediaType { }
interface NextFunction extends core.NextFunction { }
interface Request<P extends core.Params = core.ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = core.Query> extends core.Request<P, ResBody, ReqBody, ReqQuery> { }
interface RequestHandler<P extends core.Params = core.ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = core.Query> extends core.RequestHandler<P, ResBody, ReqBody, ReqQuery> { }
interface RequestParamHandler extends core.RequestParamHandler { }
export interface Response<ResBody = any> extends core.Response<ResBody> { }
interface Router extends core.Router { }
interface Send extends core.Send { }
}
export = e;

109
src/db.ts Normal file
View File

@ -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();
}
});
});
});
}
};

11
src/globals.ts Normal file
View File

@ -0,0 +1,11 @@
const globals = {
levels: [ // access levels
'read',
'write',
'maintain',
'dev',
'admin'
]
};
export default globals;

101
src/helpers/authorize.ts Normal file
View File

@ -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);
}
});
}

64
src/helpers/mail.ts Normal file
View File

@ -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);
});
}
}

90
src/helpers/test.ts Normal file
View File

@ -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;
}
}
}

View File

@ -1,37 +1,18 @@
import cfenv from 'cfenv';
import express from 'express'; import express from 'express';
import mongoose from 'mongoose'; import bodyParser from 'body-parser';
import swagger from 'swagger-ui-express'; import swagger from 'swagger-ui-express';
import jsonRefParser, {JSONSchema} from '@apidevtools/json-schema-ref-parser'; 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 // tell if server is running in debug or production environment
console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : '===== DEVELOPMENT ====='); console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
// get mongodb address from server, otherwise set to localhost
let connectionString: string = "";
if(process.env.NODE_ENV === 'production') {
let services = cfenv.getAppEnv().getServices();
for (let service in services) {
if(services[service].tags.indexOf("mongodb") >= 0) {
connectionString = services[service]["credentials"].uri;
}
}
}
else {
connectionString = 'mongodb://localhost/dfopdb';
}
mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true});
// connect to mongodb
let db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
console.log(`Connected to ${connectionString}`);
});
// mongodb connection
db.connect();
// create Express app // create Express app
const app = express(); const app = express();
@ -40,20 +21,61 @@ app.disable('x-powered-by');
// get port from environment, defaults to 3000 // get port from environment, defaults to 3000
const port = process.env.PORT || 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 // require routes
app.use('/', require('./routes/root')); 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 // Swagger UI
let oasDoc: JSONSchema = {}; let apiDoc: JSONSchema = {};
jsonRefParser.bundle('oas/oas.yaml', (err, doc) => { jsonRefParser.bundle('api/api.yaml', (err, doc) => {
if(err) throw err; if(err) throw err;
oasDoc = doc; apiDoc = doc;
oasDoc.paths = oasDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e));
swagger.setup(oasDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'}); 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 // hook up server to port
app.listen(port, () => { const server = app.listen(port, () => {
console.log(`Listening on http;//localhost:${port}`); console.log(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`);
}); });
module.exports = server;

16
src/models/material.ts Normal file
View File

@ -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);

View File

@ -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);

12
src/models/note.ts Normal file
View File

@ -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);

8
src/models/note_field.ts Normal file
View File

@ -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);

18
src/models/sample.ts Normal file
View File

@ -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);

View File

@ -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);

13
src/models/user.ts Normal file
View File

@ -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);

397
src/routes/material.spec.ts Normal file
View File

@ -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: []}
});
});
});
});

110
src/routes/material.ts Normal file
View File

@ -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;

View File

@ -1,19 +1,69 @@
import supertest from 'supertest'; import TestHelper from "../helpers/test";
import should from 'should/as-function';
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 /', () => { describe('GET /', () => {
it('returns the message object', done => { it('returns the root message', done => {
server TestHelper.request(server, done, {
.get('/') method: 'get',
.expect('Content-type', /json/) url: '/',
.expect(200) httpStatus: 200,
.end(function(err, res) { res: {status: 'API server up and running!'}
should(res.statusCode).equal(200); });
should(res.body).be.eql({message: 'API server up and running!'}); });
done(); });
describe('Unknown routes', () => {
it('return a 404 message', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/unknownroute',
httpStatus: 404
});
});
});
describe('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'}
});
}); });
}); });
}); });

View File

@ -1,9 +1,15 @@
import express from 'express'; import express from 'express';
import globals from '../globals';
const router = express.Router(); const router = express.Router();
router.get('/', (req, res) => { 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; module.exports = router;

336
src/routes/sample.spec.ts Normal file
View File

@ -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
});
});
});
});

109
src/routes/sample.ts Normal file
View File

@ -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);
})
}
});
});
}

578
src/routes/template.spec.ts Normal file
View File

@ -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
})
});
});
});
});

90
src/routes/template.ts Normal file
View File

@ -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;

626
src/routes/user.spec.ts Normal file
View File

@ -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();
});
});
});
});
});
});

173
src/routes/user.ts Normal file
View File

@ -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, <br><br> You requested to reset your password.<br>Your new password is:<br><br>' + newPass + '<br><br>If you did not request a password reset, talk to the sysadmin quickly!<br><br>Have a nice day.<br><br>The DFOP team', err => {
if (err) return next(err);
res.json({status: 'OK'});
});
});
});
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
module.exports = router;

26
src/routes/validate/id.ts Normal file
View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

281
src/test/db.json Normal file
View File

@ -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"
}
]
}
}

201
static/img/bosch-logo.svg Normal file
View File

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="bosch-lifeclip" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 435 155"
style="enable-background:new 0 0 435 155;" xml:space="preserve">
<style type="text/css">
.anker{fill:#606061;}
.bosch{fill-rule:evenodd;clip-rule:evenodd;fill:#EA0016;}
.claim{fill:#000000;}
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
.st1{fill:url(#SVGID_1_);}
.st2{fill:#942432;}
.st3{fill:#B22739;}
.st4{fill:#931915;}
.st5{fill:#AF1A19;}
.st6{fill:#D5151A;}
.st7{fill:url(#SVGID_2_);}
.st8{fill:url(#SVGID_3_);}
.st9{fill:url(#SVGID_4_);}
.st10{fill:url(#SVGID_5_);}
.st11{fill:url(#SVGID_6_);}
.st12{fill:#253783;}
.st13{fill:url(#SVGID_7_);}
.st14{fill:url(#SVGID_8_);}
.st15{fill:url(#SVGID_9_);}
.st16{fill:url(#SVGID_10_);}
.st17{fill:url(#SVGID_11_);}
.st18{fill:url(#SVGID_12_);}
.st19{fill:#159A39;}
.st20{fill:url(#SVGID_13_);}
.st21{fill:url(#SVGID_14_);}
.st22{fill:url(#SVGID_15_);}
.st23{fill:url(#SVGID_16_);}
.st24{fill:url(#SVGID_17_);}
.st25{fill:url(#SVGID_18_);}
.st26{fill:url(#SVGID_19_);}
.st27{fill:url(#SVGID_20_);}
.st28{fill:url(#SVGID_21_);}
.st29{fill:url(#SVGID_22_);}
.st30{fill:url(#SVGID_23_);}
.st31{fill:url(#SVGID_24_);}
.st32{fill:url(#SVGID_25_);}
.st33{fill:url(#SVGID_26_);}
.st34{fill:url(#SVGID_27_);}
.st35{fill:url(#SVGID_28_);}
.st36{fill:url(#SVGID_29_);}
.st37{fill:url(#SVGID_30_);}
.st38{fill:url(#SVGID_31_);}
.st39{fill:url(#SVGID_32_);}
.st40{fill:url(#SVGID_33_);}
.st41{fill:url(#SVGID_34_);}
.st42{fill:url(#SVGID_35_);}
.st43{fill:url(#SVGID_36_);}
.st44{fill:url(#SVGID_37_);}
.st45{fill:url(#SVGID_38_);}
.st46{fill:url(#SVGID_39_);}
.st47{fill:url(#SVGID_40_);}
.st48{fill:url(#SVGID_41_);}
.st49{fill:url(#SVGID_42_);}
.st50{fill:url(#SVGID_43_);}
.st51{fill:url(#SVGID_44_);}
.st52{fill:url(#SVGID_45_);}
.st53{fill:url(#SVGID_46_);}
.st54{fill:url(#SVGID_47_);}
.st55{fill:url(#SVGID_48_);}
.st56{fill:url(#SVGID_49_);}
.st57{fill:url(#SVGID_50_);}
.st58{fill:url(#SVGID_51_);}
.st59{fill:url(#SVGID_52_);}
.st60{fill:url(#SVGID_53_);}
.st61{fill:url(#SVGID_54_);}
.st62{fill:url(#SVGID_55_);}
.st63{fill:url(#SVGID_56_);}
</style>
<g id="box">
<g id="claim-english">
<path class="claim" d="M147.66699,107.6748v27.80957h-3.22852V107.6748H147.66699z"/>
<path class="claim" d="M157.896,115.14258v3.11133c1.24463-2.29492,3.30615-3.46191,6.18408-3.46191
c4.35645,0,6.729,2.83984,6.729,8.09082v12.60156h-3.22852v-12.83496c0-3.50098-1.32227-5.05664-4.31689-5.05664
c-3.26758,0-5.36768,2.13965-5.36768,5.44531v12.44629h-3.22803v-20.3418H157.896z"/>
<path class="claim" d="M178.39355,115.14258l5.44531,17.07422l5.32861-17.07422h3.34473l-6.84521,20.3418h-3.81201
l-6.96191-20.3418H178.39355z"/>
<path class="claim" d="M213.16455,132.25586c-1.8667,2.68359-4.47266,3.57812-7.73975,3.57812
c-6.45654,0-10.07373-4.23926-10.07373-10.46191c0-6.2627,3.77246-10.58008,9.25684-10.58008
c5.40625,0,8.7124,3.8125,8.7124,10.11328v1.0498H198.6958c0.07764,2.91699,0.73926,4.55078,2.13916,5.83398
c1.05029,0.93359,2.41162,1.36133,4.51172,1.36133c2.25586,0,3.88965-0.62207,5.44531-2.7998L213.16455,132.25586z
M210.01416,123.34961c-0.42773-4.16211-1.9834-5.87305-5.36719-5.87305c-3.22852,0-5.09521,1.90527-5.83447,5.87305H210.01416z"
/>
<path class="claim" d="M222.69336,115.14258v3.11133c1.24414-2.29492,3.30566-3.46191,6.18359-3.46191
c4.35645,0,6.72852,2.83984,6.72852,8.09082v12.60156h-3.22754v-12.83496c0-3.50098-1.32227-5.05664-4.31738-5.05664
c-3.26758,0-5.36719,2.13965-5.36719,5.44531v12.44629h-3.22852v-20.3418H222.69336z"/>
<path class="claim" d="M243.9668,115.14258v-5.29004l3.22754-0.77734v6.06738h5.13477v2.68359h-5.13477v12.40723
c0,1.9834,0.73926,2.91699,2.33398,2.91699c1.0498,0,1.82812-0.27148,2.76172-0.97168l1.24414,2.2168
c-1.36133,1.0498-2.52734,1.43848-4.2002,1.43848c-3.30566,0-5.36719-1.90527-5.36719-4.97852v-13.0293h-3.1123v-2.68359H243.9668
z"/>
<path class="claim" d="M274.61426,132.25586c-1.86719,2.68359-4.47266,3.57812-7.74023,3.57812
c-6.45605,0-10.07324-4.23926-10.07324-10.46191c0-6.2627,3.77246-10.58008,9.25684-10.58008
c5.40625,0,8.71191,3.8125,8.71191,10.11328v1.0498h-14.62402c0.07812,2.91699,0.73926,4.55078,2.13965,5.83398
c1.0498,0.93359,2.41113,1.36133,4.51172,1.36133c2.25586,0,3.88867-0.62207,5.44531-2.7998L274.61426,132.25586z
M271.46387,123.34961c-0.42773-4.16211-1.9834-5.87305-5.36719-5.87305c-3.22852,0-5.0957,1.90527-5.83398,5.87305H271.46387z"/>
<path class="claim" d="M294.64453,132.2168c-1.51758,2.37305-3.8125,3.61719-6.72949,3.61719
c-5.28906,0-8.71191-4.16113-8.71191-10.61816c0-6.30078,3.46191-10.42383,8.67383-10.42383
c2.87793,0,5.21094,1.28418,6.76758,3.69531v-12.29102h3.22754v29.28809h-3.22754V132.2168z M282.54785,125.41113
c0,5.13379,1.94434,7.73926,5.83398,7.73926c4.08398,0,6.37891-2.83887,6.37891-7.81738c0-4.8623-2.37207-7.85645-6.26172-7.85645
C284.6875,117.47656,282.54785,120.35449,282.54785,125.41113z"/>
<path class="claim" d="M318.79688,115.14258v-2.83984c0-4.16113,2.2168-6.53418,6.02832-6.53418
c1.36133,0,2.4502,0.27246,3.65625,0.93359l-0.81738,2.48926c-1.20508-0.58301-1.75-0.73926-2.68359-0.73926
c-1.86719,0-2.95605,1.32324-2.95605,3.50098v3.18945h4.66797v2.68359h-4.66797v17.6582h-3.22754v-17.6582h-2.91797v-2.68359
H318.79688z"/>
<path class="claim" d="M348.08398,125.29395c0,6.14551-4.00586,10.54004-9.64551,10.54004
c-5.52344,0-9.56836-4.43359-9.56836-10.50098c0-6.14551,4.00586-10.54102,9.68457-10.54102
C344.07812,114.79199,348.08398,119.22656,348.08398,125.29395z M332.21484,125.37207c0,4.78418,2.41113,7.77832,6.22363,7.77832
c3.85059,0,6.30078-3.0332,6.30078-7.81738c0-4.74512-2.4502-7.85645-6.18457-7.85645
C334.62598,117.47656,332.21484,120.43262,332.21484,125.37207z"/>
<path class="claim" d="M356.05664,115.14258v3.2666c1.32227-2.4502,3.07227-3.61719,5.44531-3.61719
c1.24414,0,2.2168,0.2334,3.30566,0.81738l-1.32227,2.72266c-0.89453-0.4668-1.43848-0.62207-2.37207-0.62207
c-3.15039,0-5.05664,2.52734-5.05664,6.61133v11.16309h-3.22852v-20.3418H356.05664z"/>
<path class="claim" d="M387.52148,134.8623c-0.97266,0.58301-1.98438,0.93359-3.5791,0.93359
c-2.95508,0-4.93945-1.32324-4.93945-5.36816v-24.23145h3.22852v24.50391c0,1.90625,1.01172,2.41113,2.2168,2.41113
c0.93359,0,1.51758-0.27148,2.02246-0.66113L387.52148,134.8623z"/>
<path class="claim" d="M395.96094,108.80273c0,1.16699-0.97266,2.13867-2.13965,2.13867s-2.13867-0.97168-2.13867-2.13867
s0.97168-2.13965,2.13867-2.13965S395.96094,107.63574,395.96094,108.80273z M395.49414,115.14258v20.3418h-3.22852v-20.3418
H395.49414z"/>
<path class="claim" d="M403.93359,115.14258v-2.83984c0-4.16113,2.2168-6.53418,6.02832-6.53418
c1.36133,0,2.4502,0.27246,3.65625,0.93359l-0.81738,2.48926c-1.20508-0.58301-1.75-0.73926-2.68359-0.73926
c-1.86719,0-2.95605,1.32324-2.95605,3.50098v3.18945h4.66797v2.68359h-4.66797v17.6582h-3.22754v-17.6582h-2.91797v-2.68359
H403.93359z"/>
<path class="claim" d="M431.89844,132.25586c-1.86719,2.68359-4.47266,3.57812-7.74023,3.57812
c-6.45605,0-10.07324-4.23926-10.07324-10.46191c0-6.2627,3.77246-10.58008,9.25684-10.58008
c5.40625,0,8.71191,3.8125,8.71191,10.11328v1.0498h-14.62402c0.07812,2.91699,0.73926,4.55078,2.13965,5.83398
c1.0498,0.93359,2.41113,1.36133,4.51172,1.36133c2.25586,0,3.88965-0.62207,5.44531-2.7998L431.89844,132.25586z
M428.74805,123.34961c-0.42773-4.16211-1.9834-5.87305-5.36719-5.87305c-3.22852,0-5.0957,1.90527-5.83398,5.87305H428.74805z"/>
</g>
<g id="bosch">
<g>
<path class="bosch" d="M185.19998,46.7c0,0,8.79999-3,8.79999-13c0-11.7-8.29999-17.5-19.70001-17.5h-29.89996v63.59999h32.5
c10,0,19.79999-7,19.79999-17.7C196.69998,49.39999,185.19998,46.8,185.19998,46.7z M160,29.39999h11.60001
c3.60001,0,6,2.39999,6,6c0,2.8-2.20001,5.8-6.29999,5.8h-11.39999L160,29.39999L160,29.39999z M171.69998,66.5h-11.60001V54
h11.30002c5.70001,0,8.39999,2.5,8.39999,6.2C179.79999,64.8,176.39999,66.5,171.69998,66.5z"/>
<path class="bosch" d="M231.10001,14.60001c-18.39999,0-29.20001,14.7-29.20001,33.3c0,18.7,10.79999,33.3,29.20001,33.3
c18.5,0,29.20001-14.60001,29.20001-33.3C260.29999,29.3,249.60001,14.60001,231.10001,14.60001z M231.10001,66
c-9,0-13.5-8.10001-13.5-18.10001s4.5-18,13.5-18s13.60001,8.10001,13.60001,18C244.69998,58,240.10001,66,231.10001,66z"/>
<path class="bosch" d="M294.19998,41.2l-2.20001-0.5c-5.39999-1.10001-9.70001-2.5-9.70001-6.39999
c0-4.2,4.10001-5.89999,7.70001-5.89999c5.29999,0,10,2.60001,13,5.89999l9.89999-9.8c-4.5-5.10001-11.79999-10-23.20001-10
c-13.39999,0-23.60001,7.5-23.60001,20c0,11.39999,8.20001,17,18.20001,19.10001l2.20001,0.5c8.29999,1.7,11.39999,3,11.39999,7
c0,3.8-3.39999,6.3-8.60001,6.3c-6.20001,0-11.79999-2.7-16.10001-8.2l-10.10001,10
c5.60001,6.7,12.70001,11.89999,26.39999,11.89999c11.89999,0,24.60001-6.8,24.60001-20.7
C314.29999,45.89999,303.29999,43.10001,294.19998,41.2z"/>
<path class="bosch" d="M349.69998,66c-7,0-14.29999-5.8-14.29999-18.5c0-11.3,6.79999-17.60001,13.89999-17.60001
c5.60001,0,8.89999,2.60001,11.5,7.10001l12.79999-8.5c-6.39999-9.7-14-13.8-24.5-13.8
c-19.20001,0-29.60001,14.89999-29.60001,32.90001c0,18.89999,11.5,33.7,29.39999,33.7
c12.60001,0,18.60001-4.39999,25.10001-13.8l-12.89999-8.7C358.5,63,355.69998,66,349.69998,66z"/>
<polygon class="bosch" points="416.30002,16.2 416.30002,39.60001 396.99997,39.60001 396.99997,16.2 380.29999,16.2
380.29999,79.8 396.99997,79.8 396.99997,54.7 416.30002,54.7 416.30002,79.8 432.99997,79.8 432.99997,16.2 "/>
</g>
<g>
<path class="bosch" d="M185.19998,46.7c0,0,8.79999-3,8.79999-13c0-11.7-8.29999-17.5-19.70001-17.5h-29.89996v63.59999h32.5
c10,0,19.79999-7,19.79999-17.7C196.69998,49.39999,185.19998,46.8,185.19998,46.7z M160,29.39999h11.60001
c3.60001,0,6,2.39999,6,6c0,2.8-2.20001,5.8-6.29999,5.8h-11.39999L160,29.39999L160,29.39999z M171.69998,66.5h-11.60001V54
h11.30002c5.70001,0,8.39999,2.5,8.39999,6.2C179.79999,64.8,176.39999,66.5,171.69998,66.5z"/>
<path class="bosch" d="M231.10001,14.60001c-18.39999,0-29.20001,14.7-29.20001,33.3c0,18.7,10.79999,33.3,29.20001,33.3
c18.5,0,29.20001-14.60001,29.20001-33.3C260.29999,29.3,249.60001,14.60001,231.10001,14.60001z M231.10001,66
c-9,0-13.5-8.10001-13.5-18.10001s4.5-18,13.5-18s13.60001,8.10001,13.60001,18C244.69998,58,240.10001,66,231.10001,66z"/>
<path class="bosch" d="M294.19998,41.2l-2.20001-0.5c-5.39999-1.10001-9.70001-2.5-9.70001-6.39999
c0-4.2,4.10001-5.89999,7.70001-5.89999c5.29999,0,10,2.60001,13,5.89999l9.89999-9.8c-4.5-5.10001-11.79999-10-23.20001-10
c-13.39999,0-23.60001,7.5-23.60001,20c0,11.39999,8.20001,17,18.20001,19.10001l2.20001,0.5c8.29999,1.7,11.39999,3,11.39999,7
c0,3.8-3.39999,6.3-8.60001,6.3c-6.20001,0-11.79999-2.7-16.10001-8.2l-10.10001,10
c5.60001,6.7,12.70001,11.89999,26.39999,11.89999c11.89999,0,24.60001-6.8,24.60001-20.7
C314.29999,45.89999,303.29999,43.10001,294.19998,41.2z"/>
<path class="bosch" d="M349.69998,66c-7,0-14.29999-5.8-14.29999-18.5c0-11.3,6.79999-17.60001,13.89999-17.60001
c5.60001,0,8.89999,2.60001,11.5,7.10001l12.79999-8.5c-6.39999-9.7-14-13.8-24.5-13.8
c-19.20001,0-29.60001,14.89999-29.60001,32.90001c0,18.89999,11.5,33.7,29.39999,33.7
c12.60001,0,18.60001-4.39999,25.10001-13.8l-12.89999-8.7C358.5,63,355.69998,66,349.69998,66z"/>
<polygon class="bosch" points="416.30002,16.2 416.30002,39.60001 396.99997,39.60001 396.99997,16.2 380.29999,16.2
380.29999,79.8 396.99997,79.8 396.99997,54.7 416.30002,54.7 416.30002,79.8 432.99997,79.8 432.99997,16.2 "/>
</g>
</g>
<g id="anker">
<g>
<path class="anker" d="M48.2,0C21.59999,0,0,21.60001,0,48.2s21.60001,48.2,48.2,48.2s48.2-21.60001,48.2-48.2S74.79999,0,48.2,0
z M48.2,91.89999c-24.10001,0-43.7-19.60001-43.7-43.7S24.10001,4.5,48.2,4.5s43.7,19.60001,43.7,43.7
S72.29999,91.89999,48.2,91.89999z"/>
<path class="anker" d="M68.09999,18.10001h-3.3v16.5H31.69998v-16.5h-3.39999c-9.7,6.5-16.2,17.5-16.2,30.10001
s6.5,23.60001,16.2,30.10001h3.39999v-16.5h33.10001v16.5h3.3c9.8-6.5,16.2-17.5,16.2-30.10001
S77.89999,24.60001,68.09999,18.10001z M27.09999,71.8c-6.7-5.89999-10.60001-14.39999-10.60001-23.60001
c0-9.2,3.89999-17.7,10.60001-23.60001V71.8z M64.79999,57.2H31.69998V39.09999h33.10001V57.2z M69.29999,71.7v-10l0,0V34.59999
l0,0v-10c6.60001,5.89999,10.5,14.39999,10.5,23.5C79.79999,57.3,75.89999,65.8,69.29999,71.7z"/>
</g>
<g>
<path class="anker" d="M48.2,0C21.59999,0,0,21.60001,0,48.2s21.60001,48.2,48.2,48.2s48.2-21.60001,48.2-48.2S74.79999,0,48.2,0
z M48.2,91.89999c-24.10001,0-43.7-19.60001-43.7-43.7S24.10001,4.5,48.2,4.5s43.7,19.60001,43.7,43.7
S72.29999,91.89999,48.2,91.89999z"/>
<path class="anker" d="M68.09999,18.10001h-3.3v16.5H31.69998v-16.5h-3.39999c-9.7,6.5-16.2,17.5-16.2,30.10001
s6.5,23.60001,16.2,30.10001h3.39999v-16.5h33.10001v16.5h3.3c9.8-6.5,16.2-17.5,16.2-30.10001
S77.89999,24.60001,68.09999,18.10001z M27.09999,71.8c-6.7-5.89999-10.60001-14.39999-10.60001-23.60001
c0-9.2,3.89999-17.7,10.60001-23.60001V71.8z M64.79999,57.2H31.69998V39.09999h33.10001V57.2z M69.29999,71.7v-10l0,0V34.59999
l0,0v-10c6.60001,5.89999,10.5,14.39999,10.5,23.5C79.79999,57.3,75.89999,65.8,69.29999,71.7z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

306
static/styles/swagger.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -4,13 +4,21 @@
"target": "es5", "target": "es5",
"outDir": "dist", "outDir": "dist",
"sourceMap": true, "sourceMap": true,
"esModuleInterop": true "esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
"diagnostics": true,
"typeRoots": [
"src/customTypings",
"node_modules/@types"
]
}, },
"files": [ "files": [
"./node_modules/@types/node/index.d.ts" "./node_modules/@types/node/index.d.ts"
], ],
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts",
"src/**/*.json"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"