diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..339dacb
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+12.17
\ No newline at end of file
diff --git a/api/root.yaml b/api/root.yaml
index 3070412..af618a7 100644
--- a/api/root.yaml
+++ b/api/root.yaml
@@ -37,6 +37,9 @@
method:
type: string
example: 'basic'
+ level:
+ type: string
+ example: read
401:
$ref: 'api.yaml#/components/responses/401'
500:
diff --git a/api/schemas.yaml b/api/schemas.yaml
index 1c844bb..2ac14ce 100644
--- a/api/schemas.yaml
+++ b/api/schemas.yaml
@@ -158,6 +158,10 @@ Template:
type: number
readOnly: true
example: 1
+ first_id:
+ readOnly: true
+ type: string
+ example: 5ea0450ed851c30a90e70894
parameters:
type: array
items:
diff --git a/build.bat b/build.bat
index d632b14..3112adb 100644
--- a/build.bat
+++ b/build.bat
@@ -1,4 +1,6 @@
call npm run tsc-full
copy package.json dist\package.json
+copy package-lock.json dist\package-lock.json
+copy .nvmrc dist\.nvmrc
Xcopy /E /I api dist\api
Xcopy /E /I static dist\static
\ No newline at end of file
diff --git a/data_import/import.js b/data_import/import.js
index 8fac949..0f8d867 100644
--- a/data_import/import.js
+++ b/data_import/import.js
@@ -158,6 +158,9 @@ async function importCsv(doc) {
newE[field] = '';
}
});
+ // if(newE['materialname'] === '') { // TODO: is this replacement okay?
+ // newE['materialname'] = newE['material'];
+ // }
if (newE['supplier'] === '') { // empty supplier fields
newE['supplier'] = 'unknown';
}
@@ -194,7 +197,8 @@ async function allDpts() {
password: 'Abc123!#'
}
});
- const measurement_template = res.data.find(e => e.name === 'spectrum')._id;
+ const measurement_templates = res.data.filter(e => e.name === 'spectrum');
+ const measurement_template = measurement_templates[measurement_templates.length - 1]._id;
res = await axios({
method: 'get',
url: host + '/samples?status=all',
@@ -207,7 +211,7 @@ async function allDpts() {
res.data.forEach(sample => {
sampleIds[sample.number] = sample._id;
});
- const dptRegex = /(.*?)_(.*?)_(\d+|\d+_\d+).DPT/;
+ const dptRegex = /(.*?)_(.*?)_(\d+|[a-zA-Z0-9]+_\d+).DPT/;
const dpts = fs.readdirSync(dptFiles);
for (let i in dpts) {
const regexRes = dptRegex.exec(dpts[i])
@@ -268,8 +272,8 @@ async function allKfVz() {
password: 'Abc123!#'
}
});
- const kf_template = res.data.find(e => e.name === 'kf')._id;
- const vz_template = res.data.find(e => e.name === 'vz')._id;
+ const kf_template = res.data.filter(e => e.name === 'kf').sort((a, b) => b.version - a.version)[0]._id;
+ const vz_template = res.data.filter(e => e.name === 'vz').sort((a, b) => b.version - a.version)[0]._id;
res = await axios({
method: 'get',
url: host + '/samples?status=all',
@@ -314,7 +318,7 @@ async function allKfVz() {
errors.push(`KF/VZ upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`);
});
}
- if (sample['VZ (ml/g)']) {
+ if (sample['vz(ml/g)']) {
await axios({
method: 'post',
url: host + '/measurement/new',
@@ -379,7 +383,7 @@ async function allSamples() {
continue;
}
samples.push({
- number: sample['samplenumber'],
+ number: sample['samplenumber'].replace(/[A-Z][a-z]0\d_/, ''), // remove leading An_01 and Eh_01
type: sampleType(sample['granulate/part']),
batch: sample['charge/batch'],
material_id: material._id,
diff --git a/package-lock.json b/package-lock.json
index 34fb53e..6749f0a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3773,17 +3773,9 @@
}
},
"swagger-ui-dist": {
- "version": "3.24.3",
- "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.24.3.tgz",
- "integrity": "sha512-kB8qobP42Xazaym7sD9g5mZuRL4416VIIYZMqPEIskkzKqbPLQGEiHA3ga31bdzyzFLgr6Z797+6X1Am6zYpbg=="
- },
- "swagger-ui-express": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.2.tgz",
- "integrity": "sha512-bVT16qj6WdNlEKFkSLOoTeGuqEm2lfOFRq6mVHAx+viA/ikORE+n4CS3WpVcYmQzM4HE6+DUFgAWcMRBJNpjcw==",
- "requires": {
- "swagger-ui-dist": "^3.18.1"
- }
+ "version": "3.30.2",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.30.2.tgz",
+ "integrity": "sha512-hAu/ig5N8i0trXXbrC7rwbXV4DhpEAsZhYXDs1305OjmDgjGC0thINbb0197idy3Pp+B6w7u426SUM43GAP7qw=="
},
"term-size": {
"version": "2.2.0",
diff --git a/package.json b/package.json
index f9494d3..7bf20ea 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"tsc": "tsc",
"tsc-full": "del /q dist\\* & (for /d %x in (dist\\*) do @rd /s /q \"%x\") & tsc",
"build": "build.bat",
- "build-push": "build.bat && cf push",
+ "build-push": "build.bat && timeout 3 && cf push",
"test": "mocha dist/**/**.spec.js",
"start": "node index.js",
"dev": "nodemon -e ts,yaml --exec \"tsc && node dist/index.js || exit 1\"",
@@ -37,7 +37,7 @@
"lodash": "^4.17.15",
"mongo-sanitize": "^1.1.0",
"mongoose": "^5.8.7",
- "swagger-ui-express": "4.1.2"
+ "swagger-ui-dist": "^3.30.2"
},
"devDependencies": {
"@types/bcrypt": "^3.0.0",
diff --git a/src/api.ts b/src/api.ts
index aab7b80..8988cf1 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -1,48 +1,131 @@
-import swagger from 'swagger-ui-express';
+import express from 'express';
+import swaggerUi from 'swagger-ui-dist';
import jsonRefParser, {JSONSchema} from '@apidevtools/json-schema-ref-parser';
import oasParser from '@apidevtools/swagger-parser';
-// modifies the normal swagger-ui-express package
+// modified from https://github.com/scottie1984/swagger-ui-express
// usage: app.use('/api-doc', api.serve(), api.setup());
// the paths property can be split using allOf
// further route documentation can be included in the x-doc property
-export default class api {
- static serve () {
- return swagger.serve;
- }
- static setup () {
- let apiDoc: JSONSchema = {};
- jsonRefParser.bundle('api/api.yaml', (err, doc) => { // parse yaml
- if (err) throw err;
- apiDoc = doc;
- apiDoc.servers.splice(process.env.NODE_ENV === 'production', 1);
- apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); // bundle routes
- apiDoc = this.resolveXDoc(apiDoc);
- oasParser.validate(apiDoc, (err, api) => { // validate oas schema
- if (err) {
- console.error(err);
- }
- else {
- console.info(process.env.NODE_ENV === 'test' ? '' : 'API ok, version ' + api.info.version);
- swagger.setup(apiDoc);
- }
- });
- });
- return swagger.setup(apiDoc, {customCssUrl: '/static/styles/swagger.css'})
- }
-
- private static resolveXDoc (doc) { // resolve x-doc properties recursively
- Object.keys(doc).forEach(key => {
- if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) { // add x-doc to description, is styled via css
- doc[key].description += 'docs
' + doc[key]['x-doc'] + ' ';
+export default function api () {
+ // generate apiDoc
+ let apiDoc: JSONSchema = {};
+ jsonRefParser.bundle('api/api.yaml', (err, doc) => { // parse yaml
+ if (err) throw err;
+ apiDoc = doc;
+ apiDoc.servers.splice(process.env.NODE_ENV === 'production', 1);
+ apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); // bundle routes
+ apiDoc = resolveXDoc(apiDoc);
+ oasParser.validate(apiDoc, (err, api) => { // validate oas schema
+ if (err) {
+ console.error(err);
}
- else if (typeof doc[key] === 'object' && doc[key] !== null) { // go deeper into recursion
- doc[key] = this.resolveXDoc(doc[key]);
+ else {
+ console.info(process.env.NODE_ENV === 'test' ? '' : 'API ok, version ' + api.info.version);
}
});
- return doc;
- }
-}
\ No newline at end of file
+ });
+
+ return [
+ (req, res, next) => { // serve init js and apiDoc file
+ switch (req.url) {
+ case '/swagger-ui-init.js':
+ res.set('Content-Type', 'application/javascript');
+ res.send(jsTplString);
+ break;
+ case '/apidoc.json':
+ res.set('Content-Type', 'application/json');
+ res.send(apiDoc);
+ break;
+ default:
+ next();
+ }
+ }, // serve swagger files
+ express.static(swaggerUi.getAbsoluteFSPath(), {index: false}),
+ (req, res) => { // serve html file as default
+ res.send(htmlTplString);
+ }
+ ];
+}
+
+
+function resolveXDoc (doc) { // resolve x-doc properties recursively
+ Object.keys(doc).forEach(key => {
+ if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) { // add x-doc to description, is styled via css
+ doc[key].description += 'docs
' + doc[key]['x-doc'] + ' ';
+ }
+ else if (typeof doc[key] === 'object' && doc[key] !== null) { // go deeper into recursion
+ doc[key] = resolveXDoc(doc[key]);
+ }
+ });
+ return doc;
+}
+
+
+// templates
+
+const htmlTplString = `
+
+
+
+
+ API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+const jsTplString = `
+window.onload = function() {
+ // Build a system
+ window.ui = SwaggerUIBundle({
+ url: '/api-doc/apidoc.json',
+ dom_id: '#swagger-ui',
+ deepLinking: true,
+ presets: [
+ SwaggerUIBundle.presets.apis,
+ SwaggerUIStandalonePreset
+ ],
+ plugins: [
+ SwaggerUIBundle.plugins.DownloadUrl
+ ],
+ layout: "StandaloneLayout",
+ });
+}
+`;
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index d6ea865..3fc4ef8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,7 +8,7 @@ import cors from 'cors';
import api from './api';
import db from './db';
-// TODO: working demo branch
+// TODO: check header, also in UI
// tell if server is running in debug or production environment
console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
@@ -24,8 +24,43 @@ app.disable('x-powered-by');
// get port from environment, defaults to 3000
const port = process.env.PORT || 3000;
-//middleware
-app.use(helmet());
+// security headers
+const defaultHeaderConfig = {
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: [`'none'`],
+ baseUri: [`'self'`],
+ formAction: [`'none'`],
+ frameAncestors: [`'none'`]
+ }
+ },
+ frameguard: {
+ action: 'deny'
+ },
+ permittedCrossDomainPolicies: true,
+ refererPolicy: true
+};
+app.use(helmet(defaultHeaderConfig));
+// special CSP header for api-doc
+app.use('/api-doc', helmet.contentSecurityPolicy({
+ ...defaultHeaderConfig,
+ directives: {
+ defaultSrc: [`'none'`],
+ scriptSrc: [`'self'`],
+ connectSrc: [`'self'`],
+ styleSrc: [`'self'`, `'unsafe-inline'`],
+ imgSrc: [`'self'`, 'data:']
+ }
+}));
+// special CSP header for the bosch-logo.svg
+app.use('/static/img/bosch-logo.svg', helmet.contentSecurityPolicy({
+ ...defaultHeaderConfig,
+ directives: {
+ styleSrc: [`'unsafe-inline'`]
+ }
+}));
+
+// middleware
app.use(contentFilter()); // filter URL query attacks
app.use(express.json({ limit: '5mb'}));
app.use(express.urlencoded({ extended: false, limit: '5mb' }));
@@ -71,7 +106,7 @@ app.use('/', require('./routes/measurement'));
app.use('/static', express.static('static'));
// Swagger UI
-app.use('/api-doc', api.serve(), api.setup());
+app.use('/api-doc', api());
app.use((req, res) => { // 404 error handling
res.status(404).json({status: 'Not found'});
diff --git a/src/routes/measurement.spec.ts b/src/routes/measurement.spec.ts
index d33bfdc..180f3ce 100644
--- a/src/routes/measurement.spec.ts
+++ b/src/routes/measurement.spec.ts
@@ -18,7 +18,7 @@ describe('/measurement', () => {
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
- res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'}
+ res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'}
});
});
it('returns the measurement for an API key', done => {
@@ -27,7 +27,7 @@ describe('/measurement', () => {
url: '/measurement/800000000000000000000001',
auth: {key: 'janedoe'},
httpStatus: 200,
- res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'}
+ res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'}
});
});
it('returns deleted measurements for a maintain/admin user', done => {
@@ -80,7 +80,7 @@ describe('/measurement', () => {
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {},
- res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'}
+ res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'}
});
});
it('keeps unchanged values', done => {
@@ -89,10 +89,10 @@ describe('/measurement', () => {
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
- req: {values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}}
+ req: {values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}}
}).end((err, res) => {
if (err) return done(err);
- should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'});
+ should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'});
MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => {
if (err) return done(err);
should(data).have.property('status',globals.status.validated);
@@ -126,7 +126,7 @@ describe('/measurement', () => {
req: {values: {dpt: [[1,2],[3,4],[5,6]]}}
}).end((err, res) => {
if (err) return done(err);
- should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[1,2],[3,4],[5,6]]}, measurement_template: '300000000000000000000001'});
+ should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[1,2],[3,4],[5,6]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'});
MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => {
should(data).have.only.keys('_id', 'sample_id', 'values', 'measurement_template', 'status', '__v');
should(data.sample_id.toString()).be.eql('400000000000000000000001');
@@ -144,7 +144,7 @@ describe('/measurement', () => {
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
- req: {values: {dpt: [[1,2],[3,4],[5,6]]}},
+ req: {values: {dpt: [[1,2],[3,4],[5,6]], device: 'Alpha I'}},
log: {
collection: 'measurements',
dataAdd: {
diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts
index 68531a5..b84d0c2 100644
--- a/src/routes/root.spec.ts
+++ b/src/routes/root.spec.ts
@@ -179,7 +179,7 @@ describe('/', () => {
url: '/authorized',
auth: {key: 'admin'},
httpStatus: 200,
- res: {status: 'Authorization successful', method: 'key'}
+ res: {status: 'Authorization successful', method: 'key', level: 'admin'}
});
});
it('works with basic auth', done => {
@@ -188,7 +188,7 @@ describe('/', () => {
url: '/authorized',
auth: {basic: 'admin'},
httpStatus: 200,
- res: {status: 'Authorization successful', method: 'basic'}
+ res: {status: 'Authorization successful', method: 'basic', level: 'admin'}
});
});
});
@@ -242,7 +242,7 @@ describe('The /api/{url} redirect', () => {
url: '/api/authorized',
auth: {basic: 'admin'},
httpStatus: 200,
- res: {status: 'Authorization successful', method: 'basic'}
+ res: {status: 'Authorization successful', method: 'basic', level: 'admin'}
});
});
it('is disabled in production', done => {
diff --git a/src/routes/root.ts b/src/routes/root.ts
index 1547844..20f10b9 100644
--- a/src/routes/root.ts
+++ b/src/routes/root.ts
@@ -14,7 +14,7 @@ router.get('/', (req, res) => {
router.get('/authorized', (req, res) => {
if (!req.auth(res, globals.levels)) return;
- res.json({status: 'Authorization successful', method: req.authDetails.method});
+ res.json({status: 'Authorization successful', method: req.authDetails.method, level: req.authDetails.level});
});
// TODO: evaluate exact changelog functionality (restoring, delting after time, etc.)
diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts
index ca62d16..d9db97a 100644
--- a/src/routes/sample.spec.ts
+++ b/src/routes/sample.spec.ts
@@ -266,7 +266,7 @@ describe('/sample', () => {
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
- should(res.body).have.lengthOf(2);
+ should(res.body.filter(e => e.spectrum.dpt)).have.lengthOf(2);
should(res.body[0].spectrum).have.property('dpt', [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]);
should(res.body[1].spectrum).have.property('dpt', [[3996.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]);
done();
diff --git a/src/routes/sample.ts b/src/routes/sample.ts
index e468a41..10694ac 100644
--- a/src/routes/sample.ts
+++ b/src/routes/sample.ts
@@ -22,12 +22,11 @@ import csv from '../helpers/csv';
const router = express.Router();
// TODO: check added filter
-// TODO: use query pointer
// TODO: convert filter value to number according to table model
// TODO: validation for filter parameters
// TODO: location/device sort/filter
-// TODO: think about material numbers
+// TODO: think about filter keys with measurement template versions
router.get('/samples', async (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
@@ -104,9 +103,9 @@ router.get('/samples', async (req, res, next) => {
if (filters.sort[0].indexOf('measurements.') >= 0) { // sorting with measurements as starting collection
collection = MeasurementModel;
const [,measurementName, measurementParam] = filters.sort[0].split('.');
- const measurementTemplate = await MeasurementTemplateModel.findOne({name: measurementName}).lean().exec().catch(err => {next(err);});
- if (measurementTemplate instanceof Error) return;
- if (!measurementTemplate) {
+ const measurementTemplates = await MeasurementTemplateModel.find({name: measurementName}).lean().exec().catch(err => {next(err);});
+ if (measurementTemplates instanceof Error) return;
+ if (!measurementTemplates) {
return res.status(400).json({status: 'Invalid body format', details: filters.sort[0] + ' not found'});
}
let sortStartValue = null;
@@ -118,7 +117,7 @@ router.get('/samples', async (req, res, next) => {
}
sortStartValue = fromSample.values[measurementParam];
}
- queryPtr[0].$match.$and.push({measurement_template: mongoose.Types.ObjectId(measurementTemplate._id)}); // find measurements to sort
+ queryPtr[0].$match.$and.push({measurement_template: {$in: measurementTemplates.map(e => e._id)}}); // find measurements to sort
if (filters.filters.find(e => e.field === filters.sort[0])) { // sorted measurement should also be filtered
queryPtr[0].$match.$and.push(...filterQueries(filters.filters.filter(e => e.field === filters.sort[0]).map(e => {e.field = 'values.' + e.field.split('.')[2]; return e; })));
}
@@ -194,7 +193,16 @@ router.get('/samples', async (req, res, next) => {
if (!fromSample) {
return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'});
}
- sortStartValue = fromSample[filters.sort[0]];
+ console.log(fromSample);
+ console.log(filters.sort[0]);
+ console.log(fromSample[filters.sort[0]]);
+ const filterKey = filters.sort[0].split('.');
+ if (filterKey.length === 2) {
+ sortStartValue = fromSample[0][filterKey[0]][filterKey[1]];
+ }
+ else {
+ sortStartValue = fromSample[0][filterKey[0]];
+ }
}
queryPtr.push(...sortQuery(filters, [filters.sort[0], '_id'], sortStartValue));
}
@@ -290,19 +298,22 @@ router.get('/samples', async (req, res, next) => {
as: 'measurements'
}});
}
- measurementTemplates.filter(e => e.name !== 'spectrum').forEach(template => { // TODO: hard coded dpt for special treatment, change later
+ measurementTemplates.forEach(template => { // TODO: hard coded dpt for special treatment, change later
queryPtr.push({$addFields: {[template.name]: {$let: { // add measurements as property [template.name], if one result, array is reduced to direct values
vars: {arr: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', mongoose.Types.ObjectId(template._id)]}}}},
in:{$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']}
}}}}, {$addFields: {[template.name]: {$cond: ['$' + template.name + '.values', '$' + template.name + '.values', template.parameters.reduce((s, e) => {s[e.name] = null; return s;}, {})]}}});
+ if (measurementFieldsFields.find(e => e === 'spectrum')) {
+ queryPtr.push({$unwind: '$spectrum'});
+ }
});
- if (measurementFieldsFields.find(e => e === 'spectrum')) { // TODO: remove hardcoded as well
- queryPtr.push(
- {$addFields: {spectrum: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', measurementTemplates.filter(e => e.name === 'spectrum')[0]._id]}}}}},
- {$addFields: {spectrum: '$spectrum.values'}},
- {$unwind: '$spectrum'}
- );
- }
+ // if (measurementFieldsFields.find(e => e === 'spectrum')) { // TODO: remove hardcoded as well
+ // queryPtr.push(
+ // {$addFields: {spectrum: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', measurementTemplates.filter(e => e.name === 'spectrum')[0]._id]}}}}},
+ // {$addFields: {spectrum: '$spectrum.values'}},
+ // {$unwind: '$spectrum'}
+ // );
+ // }
// queryPtr.push({$unset: 'measurements'});
queryPtr.push({$project: {measurements: 0}});
}
@@ -316,9 +327,8 @@ router.get('/samples', async (req, res, next) => {
projection._id = false;
}
queryPtr.push({$project: projection});
-
if (!fieldsToAdd.find(e => /spectrum\./.test(e))) { // use streaming when including spectrum files
- collection.aggregate(query).exec((err, data) => {
+ collection.aggregate(query).allowDiskUse(true).exec((err, data) => {
if (err) return next(err);
if (data[0] && data[0].count) {
res.header('x-total-items', data[0].count.length > 0 ? data[0].count[0].count : 0);
@@ -354,7 +364,7 @@ router.get('/samples', async (req, res, next) => {
res.writeHead(200, {'Content-Type': 'application/json; charset=utf-8'});
res.write('[');
let count = 0;
- const stream = collection.aggregate(query).cursor().exec();
+ const stream = collection.aggregate(query).allowDiskUse(true).cursor().exec();
stream.on('data', data => {
if (filters.fields.indexOf('added') >= 0) { // add added date
data.added = data._id.getTimestamp();
@@ -364,6 +374,9 @@ router.get('/samples', async (req, res, next) => {
}
res.write((count === 0 ? '' : ',\n') + JSON.stringify(data)); count ++;
});
+ stream.on('error', err => {
+ console.error(err);
+ });
stream.on('close', () => {
res.write(']');
res.end();
diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts
index b07014b..db924b3 100644
--- a/src/routes/template.spec.ts
+++ b/src/routes/template.spec.ts
@@ -25,10 +25,11 @@ describe('/template', () => {
const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.condition_templates.length);
should(res.body).matchEach(condition => {
- should(condition).have.only.keys('_id', 'name', 'version', 'parameters');
+ should(condition).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters');
should(condition).have.property('_id').be.type('string');
should(condition).have.property('name').be.type('string');
should(condition).have.property('version').be.type('number');
+ should(condition).have.property('first_id').be.type('string');
should(condition.parameters).matchEach(number => {
should(number).have.only.keys('name', 'range');
should(number).have.property('name').be.type('string');
@@ -62,7 +63,7 @@ describe('/template', () => {
url: '/template/condition/200000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
- res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
+ res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('rejects an API key', done => {
@@ -98,7 +99,7 @@ describe('/template', () => {
auth: {basic: 'admin'},
httpStatus: 200,
req: {},
- res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
+ res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('keeps unchanged properties', done => {
@@ -108,7 +109,7 @@ describe('/template', () => {
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', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
+ res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('keeps only one unchanged property', done => {
@@ -118,7 +119,7 @@ describe('/template', () => {
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat treatment'},
- res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
+ res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('changes the given properties', done => {
@@ -136,6 +137,7 @@ describe('/template', () => {
should(data.first_id.toString()).be.eql('200000000000000000000001');
should(data).have.property('name', 'heat aging');
should(data).have.property('version', 2);
+ should(data.first_id.toString()).be.eql('200000000000000000000001');
should(data).have.property('parameters').have.lengthOf(1);
should(data.parameters[0]).have.property('name', 'time');
should(data.parameters[0]).have.property('range');
@@ -191,7 +193,7 @@ describe('/template', () => {
req: {parameters: [{name: 'time', range: {values: [1, 2, 5]}}]}
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {values: [1, 2, 5]}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {values: [1, 2, 5]}}]});
done();
});
});
@@ -204,7 +206,7 @@ describe('/template', () => {
req: {parameters: [{name: 'time', range: {min: 1, max: 11}}]}
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {min: 1, max: 11}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {min: 1, max: 11}}]});
done();
});
});
@@ -217,7 +219,7 @@ describe('/template', () => {
req: {parameters: [{name: 'time', range: {type: 'array'}}]}
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {type: 'array'}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {type: 'array'}}]});
done();
});
});
@@ -230,7 +232,7 @@ describe('/template', () => {
req: {parameters: [{name: 'time', range: {}}]}
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {}}]});
done();
});
});
@@ -310,9 +312,10 @@ describe('/template', () => {
req: {name: 'heat treatment3', parameters: [{name: 'material', range: {values: ['copper']}}]}
}).end((err, res) => {
if (err) return done(err);
- should(res.body).have.only.keys('_id', 'name', 'version', 'parameters');
+ should(res.body).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters');
should(res.body).have.property('name', 'heat treatment3');
should(res.body).have.property('version', 1);
+ should(res.body._id).be.eql(res.body.first_id);
should(res.body).have.property('parameters').have.lengthOf(1);
should(res.body.parameters[0]).have.property('name', 'material');
should(res.body.parameters[0]).have.property('range');
@@ -336,6 +339,7 @@ describe('/template', () => {
should(data.first_id.toString()).be.eql(data._id.toString());
should(data).have.property('name', 'heat aging');
should(data).have.property('version', 1);
+ should(res.body._id).be.eql(res.body.first_id);
should(data).have.property('parameters').have.lengthOf(1);
should(data.parameters[0]).have.property('name', 'time');
should(data.parameters[0]).have.property('range');
@@ -480,8 +484,9 @@ describe('/template', () => {
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', 'version', 'parameters');
+ should(measurement).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters');
should(measurement).have.property('_id').be.type('string');
+ should(measurement).have.property('first_id').be.type('string');
should(measurement).have.property('name').be.type('string');
should(measurement).have.property('version').be.type('number');
should(measurement.parameters).matchEach(number => {
@@ -517,7 +522,7 @@ describe('/template', () => {
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
- res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: { type: 'array'}}]}
+ res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, first_id: '300000000000000000000001', parameters: [{name: 'dpt', range: { type: 'array'}}, {name: 'device', range: {}}]}
});
});
it('rejects an API key', done => {
@@ -553,7 +558,7 @@ describe('/template', () => {
auth: {basic: 'admin'},
httpStatus: 200,
req: {},
- res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: { type: 'array'}}]}
+ res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, first_id: '300000000000000000000001', parameters: [{name: 'dpt', range: { type: 'array'}}, {name: 'device', range: {}}]}
});
});
it('keeps unchanged properties', done => {
@@ -562,8 +567,8 @@ describe('/template', () => {
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
- req: {name: 'spectrum', parameters: [{name: 'dpt', range: { type: 'array'}}]},
- res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: {type: 'array'}}]}
+ req: {name: 'spectrum', parameters: [{name: 'dpt', range: { type: 'array'}}, {name: 'device', range: {}}]},
+ res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, first_id: '300000000000000000000001', parameters: [{name: 'dpt', range: {type: 'array'}}, {name: 'device', range: {}}]}
});
});
it('keeps only one unchanged property', done => {
@@ -573,7 +578,7 @@ describe('/template', () => {
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'spectrum'},
- res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: {type: 'array'}}]}
+ res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, first_id: '300000000000000000000001', parameters: [{name: 'dpt', range: {type: 'array'}}, {name: 'device', range: {}}]}
});
});
it('changes the given properties', done => {
@@ -585,7 +590,7 @@ describe('/template', () => {
req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'IR spectrum', version: 2, parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'IR spectrum', version: 2, first_id: '300000000000000000000001', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]});
TemplateMeasurementModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
@@ -626,17 +631,19 @@ describe('/template', () => {
req: {name: 'IR spectrum'},
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'IR spectrum', version: 2, parameters: [{name: 'dpt', range: {type: 'array'}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'IR spectrum', version: 2, first_id: '300000000000000000000001', parameters: [{name: 'dpt', range: {type: 'array'}}, {name: 'device', range: {}}]});
TemplateMeasurementModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
should(data.first_id.toString()).be.eql('300000000000000000000001');
should(data).have.property('name', 'IR spectrum');
should(data).have.property('version', 2);
- should(data).have.property('parameters').have.lengthOf(1);
+ should(data).have.property('parameters').have.lengthOf(2);
should(data.parameters[0]).have.property('name', 'dpt');
should(data.parameters[0]).have.property('range');
should(data.parameters[0].range).have.property('type', 'array');
+ should(data.parameters[1]).have.property('name', 'device');
+ should(data.parameters[1]).have.property('range');
done();
});
});
@@ -650,7 +657,7 @@ describe('/template', () => {
req: {parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]}
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'spectrum', version: 2, parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'spectrum', version: 2, first_id: '300000000000000000000001', parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]});
done();
});
});
@@ -663,7 +670,7 @@ describe('/template', () => {
req: {parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]}
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'spectrum', version: 2, parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'spectrum', version: 2, first_id: '300000000000000000000001', parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]});
done();
});
});
@@ -676,7 +683,7 @@ describe('/template', () => {
req: {parameters: [{name: 'dpt2', range: {type: 'array'}}]}
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'spectrum', version: 2, parameters: [{name: 'dpt2', range: {type: 'array'}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'spectrum', version: 2, first_id: '300000000000000000000001', parameters: [{name: 'dpt2', range: {type: 'array'}}]});
done();
});
});
@@ -689,7 +696,7 @@ describe('/template', () => {
req: {parameters: [{name: 'weight %', range: {}}]}
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'kf', version: 2, parameters: [{name: 'weight %', range: {}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'kf', version: 2, first_id: '300000000000000000000002', parameters: [{name: 'weight %', range: {}}]});
done();
});
});
@@ -759,9 +766,10 @@ describe('/template', () => {
req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]}
}).end((err, res) => {
if (err) return done(err);
- should(res.body).have.only.keys('_id', 'name', 'version', 'parameters');
+ should(res.body).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters');
should(res.body).have.property('name', 'vz');
should(res.body).have.property('version', 1);
+ should(res.body._id).be.eql(res.body.first_id);
should(res.body).have.property('parameters').have.lengthOf(1);
should(res.body.parameters[0]).have.property('name', 'vz');
should(res.body.parameters[0]).have.property('range');
@@ -909,7 +917,7 @@ describe('/template', () => {
const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.material_templates.length);
should(res.body).matchEach(measurement => {
- should(measurement).have.only.keys('_id', 'name', 'version', 'parameters');
+ should(measurement).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters');
should(measurement).have.property('_id').be.type('string');
should(measurement).have.property('name').be.type('string');
should(measurement).have.property('version').be.type('number');
diff --git a/src/routes/template.ts b/src/routes/template.ts
index 20f1b3b..5641d1b 100644
--- a/src/routes/template.ts
+++ b/src/routes/template.ts
@@ -44,7 +44,14 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
const {error, value: template} = TemplateValidate.input(req.body, 'change');
if (error) return res400(error, res);
- const templateData = await model(req).findById(req.params.id).lean().exec().catch(err => {next(err);}) as any;
+ // find given template
+ const templateRef = await model(req).findById(req.params.id).lean().exec().catch(err => {next(err);}) as any;
+ if (templateRef instanceof Error) return;
+ if (!templateRef) {
+ return res.status(404).json({status: 'Not found'});
+ }
+ // find latest version
+ const templateData = await model(req).findOne({first_id: templateRef.first_id}).sort({version: -1}).lean().exec().catch(err => {next(err);}) as any;
if (templateData instanceof Error) return;
if (!templateData) {
return res.status(404).json({status: 'Not found'});
diff --git a/src/routes/user.ts b/src/routes/user.ts
index 65c41d5..2393150 100644
--- a/src/routes/user.ts
+++ b/src/routes/user.ts
@@ -65,6 +65,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => {
});
});
+// TODO: only possible if no data is linked to user, otherwise change status, etc.
router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts
index 3e9aed3..19f6b50 100644
--- a/src/routes/validate/sample.ts
+++ b/src/routes/validate/sample.ts
@@ -62,9 +62,8 @@ export default class SampleValidate {
'material.name',
'material.supplier',
'material.group',
- 'material.number',
'material.properties.*',
- 'measurements.(?!spectrum)*'
+ 'measurements.(?!spectrum\.dpt)*'
];
private static fieldKeys = [
diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts
index 0721bd7..aed8f68 100644
--- a/src/routes/validate/template.ts
+++ b/src/routes/validate/template.ts
@@ -65,6 +65,7 @@ export default class TemplateValidate {
_id: IdValidate.get(),
name: this.template.name,
version: this.template.version,
+ first_id: IdValidate.get(),
parameters: this.template.parameters
}).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
diff --git a/src/test/db.json b/src/test/db.json
index 7b0fab9..7930a94 100644
--- a/src/test/db.json
+++ b/src/test/db.json
@@ -403,7 +403,8 @@
[3997.12558,98.00555],
[3995.08519,98.03253],
[3993.04480,98.02657]
- ]
+ ],
+ "device": "Alpha I"
},
"status": 10,
"measurement_template": {"$oid":"300000000000000000000001"},
@@ -470,7 +471,8 @@
[3996.12558,98.00555],
[3995.08519,98.03253],
[3993.04480,98.02657]
- ]
+ ],
+ "device": "Alpha II"
},
"status": 10,
"measurement_template": {"$oid":"300000000000000000000001"},
@@ -551,6 +553,10 @@
"range": {
"type": "array"
}
+ },
+ {
+ "name": "device",
+ "range": {}
}
],
"__v": 0
diff --git a/static/img/favicon.ico b/static/img/favicon.ico
new file mode 100644
index 0000000..41ab513
Binary files /dev/null and b/static/img/favicon.ico differ
diff --git a/static/img/header.svg b/static/img/header.svg
new file mode 100644
index 0000000..85e56b9
--- /dev/null
+++ b/static/img/header.svg
@@ -0,0 +1,82 @@
+
\ No newline at end of file
diff --git a/static/styles/swagger-ui.css b/static/styles/swagger-ui.css
new file mode 100644
index 0000000..70bf2b8
--- /dev/null
+++ b/static/styles/swagger-ui.css
@@ -0,0 +1,345 @@
+/*Bosch styling for swagger*/
+
+/*GET: dark blue*/
+/*POST: dark green*/
+/*PUT: turquoise*/
+/*DELETE: fuchsia*/
+
+:root {
+ --red: #ea0016;
+ --dark-blue: #005691;
+ --dark-blue-w75: #bfd5e3;
+ --dark-green: #006249;
+ --dark-green-w75: #bfd8d1;
+ --turquoise: #00a8b0;
+ --turquoise-w75: #bfe9eb;
+ --fuchsia: #b90276;
+ --fuchsia-w75: #edc0dd;
+ --light-grey: #bfc0c2;
+ --light-grey-w75: #efeff0;
+ --light-green: #78be20;
+}
+
+body {
+ background: #fff;
+}
+
+body:before {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 16px;
+ content: '';
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-image: url(/static/img/header.svg);
+}
+
+body:after {
+ position: absolute;
+ right: 25px;
+ top: 36px;
+ width: 135px;
+ height: 48px;
+ content: '';
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-image: url(/static/img/bosch-logo.svg);
+}
+
+.swagger-ui {
+ font-family: "Bosch Sans", sans-serif;
+}
+
+/*custom docs*/
+.docs {
+ position: relative;
+ font-size: 14px;
+}
+
+.docs > summary {
+ position: absolute;
+ right: 0;
+ top: -25px;
+ cursor: pointer;
+}
+
+.docs-open:hover {
+ text-decoration: underline;
+}
+
+/*Remove topbar*/
+.swagger-ui .topbar {
+ display: none
+}
+
+/*Remove models view*/
+.swagger-ui .models {
+ display: none;
+}
+
+/*Remove application/json select*/
+.swagger-ui .opblock .opblock-section-header > label, .swagger-ui .response-controls {
+ display: none;
+}
+
+/*Remove border radius*/
+.swagger-ui .opblock, .swagger-ui .opblock .opblock-summary-method, .swagger-ui select {
+ border-radius: 0;
+ box-shadow: none;
+}
+
+/*remove links in response*/
+.swagger-ui .response-col_links {
+ display: none;
+}
+
+/*remove version*/
+.swagger-ui .info .title span {
+ display: none;
+}
+
+/*separator before methods*/
+.swagger-ui .scheme-container {
+ box-shadow: none;
+ border-bottom: 1px solid var(--light-grey);
+}
+
+/*tag separator*/
+.swagger-ui .opblock-tag {
+ border-bottom: 1px solid var(--light-grey);
+}
+
+/*parameters/responses bar*/
+.swagger-ui .opblock .opblock-section-header {
+ box-shadow: none;
+ background: #fff;
+}
+
+/*select*/
+.swagger-ui select {
+ background-color: var(--light-grey-w75);
+ border: none;
+ height: 36px;
+}
+
+/*button*/
+.swagger-ui .btn {
+ border-radius: 0;
+ box-shadow: none;
+}
+
+.swagger-ui .btn:hover {
+ box-shadow: none;
+}
+
+/*authorize button */
+.swagger-ui .btn.authorize {
+ color: var(--light-green);
+ border-color: var(--light-green);
+}
+
+.swagger-ui .btn.authorize svg {
+ fill: var(--light-green);
+}
+
+/*auth inputs*/
+.swagger-ui .auth-container input[type="password"], .swagger-ui .auth-container input[type="text"] {
+ border-radius: 0;
+ box-shadow: none;
+ border-color: var(--light-grey);
+}
+
+.swagger-ui .dialog-ux .modal-ux {
+ border-radius: 0;
+}
+
+/*cancel button*/
+.swagger-ui .btn.cancel {
+ color: var(--red);
+ border-color: var(--red);
+}
+
+/*clipboard button*/
+.swagger-ui .copy-to-clipboard {
+ border-radius: 0;
+ top: 19px;
+ height: 28px;
+}
+.swagger-ui .copy-to-clipboard > button {
+ position: relative;
+ bottom: 3px;
+}
+.swagger-ui .curl-command .copy-to-clipboard {
+ border-radius: 0;
+ top: 24px;
+}
+.swagger-ui .curl-command .copy-to-clipboard > button {
+ position: relative;
+ bottom: 7px;
+ right: 1px;
+}
+
+/*download button*/
+.swagger-ui .download-contents {
+ border-radius: 0;
+ height: 28px;
+ width: 80px;
+}
+
+/*model*/
+.swagger-ui .model-box {
+ border-radius: 0;
+}
+
+/*execute button*/
+.swagger-ui .btn.execute {
+ background-color: var(--dark-blue);
+ border-color: var(--dark-blue);
+ height: 30px;
+ line-height: 0.7;
+}
+
+.swagger-ui .btn-group .btn:last-child {
+ border-radius: 0;
+ height: 30px;
+ border-color: var(--dark-blue);
+}
+
+.swagger-ui .btn-group .btn:first-child {
+ border-radius: 0;
+}
+
+.swagger-ui .btn-group {
+ padding: 0 20px;
+}
+
+/*parameter input*/
+.swagger-ui .parameters-col_description input[type="text"] {
+ border-radius: 0;
+}
+
+/*required label*/
+.swagger-ui .parameter__name.required > span {
+ color: var(--red) !important;
+}
+
+.swagger-ui .parameter__name.required::after {
+ color: var(--red);
+}
+/*Remove colored parameters bar*/
+.swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after {
+ background: none;
+}
+
+/*code*/
+.swagger-ui .opblock-body pre.microlight {
+ border-radius: 0;
+ background: #41444e !important;
+ padding: 0.5em;
+}
+
+.swagger-ui .highlight-code > .microlight {
+ min-height: 0;
+}
+
+/*request body*/
+.swagger-ui textarea {
+ border-radius: 0;
+}
+
+/*parameters smaller padding*/
+.swagger-ui .execute-wrapper {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+.swagger-ui .btn.execute {
+ margin-bottom: 20px;
+}
+
+.swagger-ui .opblock-description-wrapper {
+ margin-top: 20px;
+}
+
+.swagger-ui .opblock-description-wrapper {
+ margin-top: 5px;
+}
+
+.opblock-section .opblock-section-request-body > div > div {
+ padding-top: 18px;
+}
+
+/*response element positions*/
+.swagger-ui .model-example {
+ position: relative;
+ margin-top: 0;
+}
+
+.swagger-ui .tab {
+ position: absolute;
+ top: -35px;
+ right: 0;
+}
+
+.swagger-ui table tbody tr td {
+ padding: 0;
+}
+
+.swagger-ui .renderedMarkdown p {
+ margin: 8px auto;
+}
+
+/*Method colors*/
+.swagger-ui .opblock.opblock-get .opblock-summary-method {
+ background: var(--dark-blue);
+}
+
+.swagger-ui .opblock.opblock-get .opblock-summary {
+ border-color: var(--dark-blue);
+}
+
+.swagger-ui .opblock.opblock-get {
+ background: var(--dark-blue-w75);
+ border-color: var(--dark-blue);
+}
+
+.swagger-ui .opblock.opblock-post .opblock-summary-method {
+ background: var(--dark-green);
+}
+
+.swagger-ui .opblock.opblock-post .opblock-summary {
+ border-color: var(--dark-green);
+}
+
+.swagger-ui .opblock.opblock-post {
+ background: var(--dark-green-w75);
+ border-color: var(--dark-green);
+}
+
+.swagger-ui .opblock.opblock-put .opblock-summary-method {
+ background: var(--turquoise);
+}
+
+.swagger-ui .opblock.opblock-put .opblock-summary {
+ border-color: var(--turquoise);
+}
+
+.swagger-ui .opblock.opblock-put {
+ background: var(--turquoise-w75);
+ border-color: var(--turquoise);
+}
+
+.swagger-ui .opblock.opblock-delete .opblock-summary-method {
+ background: var(--fuchsia);
+}
+
+.swagger-ui .opblock.opblock-delete .opblock-summary {
+ border-color: var(--fuchsia);
+}
+
+.swagger-ui .opblock.opblock-delete {
+ background: var(--fuchsia-w75);
+ border-color: var(--fuchsia);
+}
\ No newline at end of file
diff --git a/static/styles/swagger.css b/static/styles/swagger.css
deleted file mode 100644
index 9760ed4..0000000
--- a/static/styles/swagger.css
+++ /dev/null
@@ -1,323 +0,0 @@
-/*Bosch styling for swagger*/
-
-/*GET: dark blue*/
-/*POST: dark green*/
-/*PUT: turquoise*/
-/*DELETE: fuchsia*/
-
-:root {
- --red: #ea0016;
- --dark-blue: #005691;
- --dark-blue-w75: #bfd5e3;
- --dark-green: #006249;
- --dark-green-w75: #bfd8d1;
- --turquoise: #00a8b0;
- --turquoise-w75: #bfe9eb;
- --fuchsia: #b90276;
- --fuchsia-w75: #edc0dd;
- --light-grey: #bfc0c2;
- --light-grey-w75: #efeff0;
- --light-green: #78be20;
-}
-
-body {
- background: #fff;
-}
-
-body:before {
- position: absolute;
- left: 0;
- top: 0;
- width: 100%;
- height: 16px;
- content: '';
- background-repeat: no-repeat;
- background-size: cover;
- background-image: url(data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB4bWw6c3BhY2U9InByZXNlcnZlIgogIGhlaWdodD0iMzAwIgogIHdpZHRoPSI3MjAiCiAgdmVyc2lvbj0iMS4xIgogIHk9IjAiCiAgeD0iMCIKICB2aWV3Qm94PSIwIDAgNzIwIDMwMCI+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgIC5zdDAgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF8xXyIpOwogICAgfQogICAgLnN0MSB7CiAgICAgIGZpbGw6IHVybCgiI1NWR0lEXzJfIik7CiAgICB9CiAgICAuc3QyIHsKICAgICAgZmlsbDogdXJsKCIjU1ZHSURfM18iKTsKICAgIH0KICAgIC5zdDMgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF80XyIpOwogICAgfQogICAgLnN0NCB7CiAgICAgIGZpbGw6IHVybCgiI1NWR0lEXzVfIik7CiAgICB9CiAgICAuc3Q1IHsKICAgICAgZmlsbDogI0FGMjAyNDsKICAgIH0KICAgIC5zdDYgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF82XyIpOwogICAgfQogICAgLnN0NyB7CiAgICAgIGZpbGw6ICM5NDFCMUU7CiAgICB9CiAgICAuc3Q4IHsKICAgICAgZmlsbDogI0IxMjczOTsKICAgIH0KICAgIC5zdDkgewogICAgICBmaWxsOiAjOTUyNDMyOwogICAgfQogICAgLnN0MTAgewogICAgICBmaWxsOiAjRDQyMDI3OwogICAgfQogICAgLnN0MTEgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF83XyIpOwogICAgfQogICAgLnN0MTIgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF84XyIpOwogICAgfQogICAgLnN0MTMgewogICAgICBmaWxsOiAjMUM5QTQ4OwogICAgfQogICAgLnN0MTQgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF85XyIpOwogICAgfQogICAgLnN0MTUgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF8xMF8iKTsKICAgIH0KICAgIC5zdDE2IHsKICAgICAgZmlsbDogIzJBMzg4NjsKICAgIH0KICAgIC5zdDE3IHsKICAgICAgZmlsbDogdXJsKCIjU1ZHSURfMTFfIik7CiAgICB9CiAgICAuc3QxOCB7CiAgICAgIGZpbGw6IHVybCgiI1NWR0lEXzEyXyIpOwogICAgfQogICAgLnN0MTkgewogICAgICBmaWxsOiB1cmwoIiNTVkdJRF8xM18iKTsKICAgIH0KICAgIC5zdDIwIHsKICAgICAgZmlsbDogdXJsKCIjU1ZHSURfMTRfIik7CiAgICB9CiAgPC9zdHlsZT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMS41NSwtMy4zKSI+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzFfIiB5Mj0iLTMyLjY2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHkxPSItMzIuNjYzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9Ijg0Mi4wOCIgeDE9IjExOC45OCI+PHN0b3Agc3RvcC1jb2xvcj0iIzk1MjMzMSIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzkyMUMxRCIgb2Zmc2V0PSIuMDM2MDk0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iI0IwMjczOSIgb2Zmc2V0PSIuMDg0NjQ5Ii8+PHN0b3Agc3RvcC1jb2xvcj0iI0FEMUYyNCIgb2Zmc2V0PSIuMTIzNyIvPjxzdG9wIHN0b3AtY29sb3I9IiNDNzIwMjYiIG9mZnNldD0iLjE1MDkiLz48c3RvcCBzdG9wLWNvbG9yPSIjRDQyMDI3IiBvZmZzZXQ9Ii4xNjk3Ii8+PHN0b3Agc3RvcC1jb2xvcj0iI0NDMjQzMSIgb2Zmc2V0PSIuMTc1OCIvPjxzdG9wIHN0b3AtY29sb3I9IiNCNzJCNEMiIG9mZnNldD0iLjE4ODgiLz48c3RvcCBzdG9wLWNvbG9yPSIjOTUzMzcxIiBvZmZzZXQ9Ii4yMDc0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzg4MzU3RiIgb2Zmc2V0PSIuMjE0MiIvPjxzdG9wIHN0b3AtY29sb3I9IiM4NTM2ODEiIG9mZnNldD0iLjI0MzYiLz48c3RvcCBzdG9wLWNvbG9yPSIjNkYzNjhCIiBvZmZzZXQ9Ii4yNjM4Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzM5NDI4RiIgb2Zmc2V0PSIuMjkxMSIvPjxzdG9wIHN0b3AtY29sb3I9IiMyMzNEN0QiIG9mZnNldD0iLjMyNDIiLz48c3RvcCBzdG9wLWNvbG9yPSIjMzIyQzZGIiBvZmZzZXQ9Ii40MTgxIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzJBMzg4NSIgb2Zmc2V0PSIuNDk0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzFENjJBMSIgb2Zmc2V0PSIuNTU4MSIvPjxzdG9wIHN0b3AtY29sb3I9IiMyNzZDQTUiIG9mZnNldD0iLjU3MDIiLz48c3RvcCBzdG9wLWNvbG9yPSIjNDM4RUIzIiBvZmZzZXQ9Ii42MTAzIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzU1QTVCQyIgb2Zmc2V0PSIuNjM5OSIvPjxzdG9wIHN0b3AtY29sb3I9IiM1Q0FGQkYiIG9mZnNldD0iLjY1NTYiLz48c3RvcCBzdG9wLWNvbG9yPSIjNTZBQkJEIiBvZmZzZXQ9Ii42Nzc3Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzQzOUZCOCIgb2Zmc2V0PSIuNzA1OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMxODhFQUYiIG9mZnNldD0iLjczNzIiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDM4QkFFIiBvZmZzZXQ9Ii43NDI2Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzA2OTI5MiIgb2Zmc2V0PSIuNzg5OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMwNUExNEIiIG9mZnNldD0iLjg4NzUiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDM5MjdFIiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxyZWN0IHdpZHRoPSI3MjMuMSIgeT0iMCIgeD0iMCIgaGVpZ2h0PSIzMDYuNCIgY2xhc3M9InN0MCIgZmlsbD0idXJsKCNTVkdJRF8xXykiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iU1ZHSURfMl8iIHkyPSItMTA5LjI2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9Ii0xMDkuMjYiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgLTExOC45OCAxMjAuNTQpIiB4Mj0iMjM1Ljk4IiB4MT0iMzI1LjA4Ij48c3RvcCBzdG9wLWNvbG9yPSIjODkzNjgwIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjODkzNjgwIiBvZmZzZXQ9Ii4zMzU0Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzhEMzE2RCIgb2Zmc2V0PSIuNTAyNSIvPjxzdG9wIHN0b3AtY29sb3I9IiM5MDI5NEQiIG9mZnNldD0iLjgzOTgiLz48c3RvcCBzdG9wLWNvbG9yPSIjOTAyNTQxIiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50Pjxwb2x5Z29uIHBvaW50cz0iMTc1LjEgMTUzLjIgMTE3IDMwNi40IDIwNi4xIDMwNi40IiBmaWxsPSJ1cmwoI1NWR0lEXzJfKSIgY2xhc3M9InN0MSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8zXyIgeTI9Ii04Mi4yODQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjI0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjQ0Ni41NSIgeDE9IjQ3OC45MyI+PHN0b3Agc3RvcC1jb2xvcj0iIzMyMkM2RiIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzMyMkM2RiIgb2Zmc2V0PSIuMjQyNyIvPjxzdG9wIHN0b3AtY29sb3I9IiMzMDJGNzIiIG9mZnNldD0iLjQ1OTkiLz48c3RvcCBzdG9wLWNvbG9yPSIjMkEzQTdFIiBvZmZzZXQ9Ii43MTU1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzE1NEE5MyIgb2Zmc2V0PSIuOTg5NiIvPjxzdG9wIHN0b3AtY29sb3I9IiMxMzRCOTQiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBvbHlnb24gcG9pbnRzPSIyODguNCAxNTMuMiAzMTAuNyAzMDYuNCAzNTguMSAzMDYuNCAzNTguMSAwIDMxMi45IDAiIGZpbGw9InVybCgjU1ZHSURfM18pIiBjbGFzcz0ic3QyIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzRfIiB5Mj0iLTMyLjY2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHkxPSItMzIuNjYzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjM3Mi44OCIgeDE9IjI5NC4wOCI+PHN0b3Agc3RvcC1jb2xvcj0iIzZGMzc4RCIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzNBNDI5MSIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cG9seWdvbiBwb2ludHM9IjE3NS4xIDE1My4yIDIwNi4xIDMwNi40IDI1My45IDE1My4yIDIwOS40IDAgMjA5LjQgMCIgZmlsbD0idXJsKCNTVkdJRF80XykiIGNsYXNzPSJzdDMiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iU1ZHSURfNV8iIHkyPSItMzIuNjYzIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9Ii0zMi42NjMiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgLTExOC45OCAxMjAuNTQpIiB4Mj0iMzI1LjA4IiB4MT0iNDMxLjg4Ij48c3RvcCBzdG9wLWNvbG9yPSIjMjMzRDdEIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjMjkzRDdEIiBvZmZzZXQ9Ii4yNDk1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzNBM0M4MCIgb2Zmc2V0PSIuNTQ0NiIvPjxzdG9wIHN0b3AtY29sb3I9IiM1MTNCODQiIG9mZnNldD0iLjg2MTYiLz48c3RvcCBzdG9wLWNvbG9yPSIjNUQzQTg2IiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50Pjxwb2x5Z29uIHBvaW50cz0iMjUzLjkgMTUzLjIgMjA2LjEgMzA2LjQgMzEwLjcgMzA2LjQgMjg4LjQgMTUzLjIgMzEyLjkgMCAyMDkuNCAwIiBmaWxsPSJ1cmwoI1NWR0lEXzVfKSIgY2xhc3M9InN0NCIvPjxwb2x5Z29uIHBvaW50cz0iMTE2LjEgMCA1NS43IDAgNTUuNyA5NC44IDg5LjkgMTUzLjIgNTUuNyAyMTEuNiA1NS43IDMwNi40IDExNyAzMDYuNCA5NS4yIDE1My4yIiBmaWxsPSIjYWYyMDI0IiBjbGFzcz0ic3Q1Ii8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzZfIiB5Mj0iNDMuOTM3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9IjQzLjkzNyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMTE4Ljk4IDEyMC41NCkiIHgyPSIyMzIuNjciIHgxPSIzMjkuMTEiPjxzdG9wIHN0b3AtY29sb3I9IiM4OTM2ODAiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiM4OTM2ODAiIG9mZnNldD0iLjMzNTQiLz48c3RvcCBzdG9wLWNvbG9yPSIjOEQzMTZEIiBvZmZzZXQ9Ii41MDI1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzkwMjk0RCIgb2Zmc2V0PSIuODM5OCIvPjxzdG9wIHN0b3AtY29sb3I9IiM5MDI1NDEiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBvbHlnb24gcG9pbnRzPSIxNzUuMSAxNTMuMiAyMDkuNCAwIDExNi4xIDAiIGZpbGw9InVybCgjU1ZHSURfNl8pIiBjbGFzcz0ic3Q2Ii8+PHBvbHlnb24gcG9pbnRzPSI1NS43IDk0LjggNTUuNyAwIDAgMCIgZmlsbD0iIzk0MWIxZSIgY2xhc3M9InN0NyIvPjxwb2x5Z29uIHBvaW50cz0iNTUuNyAyMTEuNiA4OS45IDE1My4yIDU1LjcgOTQuOCIgZmlsbD0iI2IxMjczOSIgY2xhc3M9InN0OCIvPjxwb2x5Z29uIHBvaW50cz0iNTUuNyAyMTEuNiAwIDMwNi40IDU1LjcgMzA2LjQiIGZpbGw9IiM5NDFiMWUiIGNsYXNzPSJzdDciLz48cG9seWdvbiBwb2ludHM9IjU1LjcgOTQuOCAwIDAgMCAzMDYuNCA1NS43IDIxMS42IiBmaWxsPSIjOTUyNDMyIiBjbGFzcz0ic3Q5Ii8+PHBvbHlnb24gcG9pbnRzPSIxMTYuMSAwIDk1LjIgMTUzLjIgMTE3IDMwNi40IDE3NS4xIDE1My4yIiBmaWxsPSIjZDQyMDI3IiBjbGFzcz0ic3QxMCIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF83XyIgeTI9Ii0xODYuMDYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjQ0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9Ijc0OC45NiIgeDE9Ijc0OC45NiI+PHN0b3Agc3RvcC1jb2xvcj0iIzk0QkU1NSIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzkzQkQ1OCIgb2Zmc2V0PSIuMDQ0MzQwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzhCQkM2QSIgb2Zmc2V0PSIuMzg5MSIvPjxzdG9wIHN0b3AtY29sb3I9IiM4NkJDNzUiIG9mZnNldD0iLjcxNDkiLz48c3RvcCBzdG9wLWNvbG9yPSIjODRCQzc5IiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxwYXRoCiAgICAgIGQ9Im02NDEuNiAyNTkuNmMxLjctMjUuNCAxMC01NC42IDE4LjgtODUuNiAxLjQtNSAyLjgtMTAgNC4yLTE1LjEtMS40LTUuNS0yLjgtMTAuOS00LjItMTYuMi04LjgtMzMuMy0xNy02NC43LTE4LjgtOTItMS40LTIxLjIgMS40LTM3IDguOS01MC42aC00NS45Yy03LjUgMTguMy0xMC4zIDI5LjEtOC45IDUwLjMgMS43IDI3LjMgMTAgNTguNyAxOC44IDkyIDEzIDQ5LjMgMjggMTA2LjIgMjMuMiAxNjQuMmgxMi45Yy03LjYtMTIuOC0xMC40LTI3LjMtOS00N3oiCiAgICAgIGNsYXNzPSJzdDExIgogICAgICBmaWxsPSJ1cmwoI1NWR0lEXzdfKSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF84XyIgeTI9Ii0xODQuNDUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTE3LjI5IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjczMy40OSIgeDE9IjY1My43NiI+PHN0b3Agc3RvcC1jb2xvcj0iIzA4QTI0QiIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzBBQTE0RSIgb2Zmc2V0PSIuMTY3OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMwQjlFNTciIG9mZnNldD0iLjQwNDciLz48c3RvcCBzdG9wLWNvbG9yPSIjMDk5QTY3IiBvZmZzZXQ9Ii42ODI3Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzA0OTQ3RCIgb2Zmc2V0PSIuOTg5OCIvPjxzdG9wIHN0b3AtY29sb3I9IiMwNDkzN0UiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0ibTYxNC41IDE0Mi4zYy04LjgtMzMuMy0xNy02NC43LTE4LjgtOTItMS40LTIxLjIgMS40LTMyIDguOS01MC4zaC0zNS40YzUuNyA1My45LTMuOCAxMDYuNy0xMy42IDE2Ni44LTUuNyAzNS0xMS43IDcxLjMtMTMuMiAxMDAuNi0xLjEgMjEuMSAwLjQgMzIuOCAxLjggMzloOTMuNWM0LjgtNTcuOS0xMC4zLTExNC44LTIzLjItMTY0LjF6IiBjbGFzcz0ic3QxMiIgZmlsbD0idXJsKCNTVkdJRF84XykiLz48cGF0aCBjbGFzcz0ic3QxMyIgZmlsbD0iIzFjOWE0OCIgZD0ibTY2NC42IDE1OC45Yy0xLjQgNS4xLTIuOCAxMC4xLTQuMiAxNS4xLTguOCAzMS0xNyA2MC4yLTE4LjggODUuNi0xLjQgMTkuNyAxLjQgMzQuMiA5IDQ2LjloMzNjNC4yLTUxLjgtNy4yLTEwMi4zLTE5LTE0Ny42eiIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF85XyIgeTI9Ii0xODUuOTYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjU0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjgxMi44MyIgeDE9IjgxMi44MyI+PHN0b3Agc3RvcC1jb2xvcj0iIzY5QTA2MCIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzYzOUQ1QyIgb2Zmc2V0PSIuMDM5ODk1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzRDOTQ0RiIgb2Zmc2V0PSIuMjE5MiIvPjxzdG9wIHN0b3AtY29sb3I9IiMzNzhFNDciIG9mZnNldD0iLjQxODQiLz48c3RvcCBzdG9wLWNvbG9yPSIjMjk4QjQ0IiBvZmZzZXQ9Ii42NTE1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzIzOEE0MyIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJtNjgwLjUgMGMxMC43IDU1LjMtMi41IDExMC40LTE1LjkgMTU4LjkgMTEuNyA0NS4zIDIzLjIgOTUuOCAxOC45IDE0Ny42aDM5LjZ2LTMwNi41aC00Mi42eiIgY2xhc3M9InN0MTQiIGZpbGw9InVybCgjU1ZHSURfOV8pIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzEwXyIgeTI9Ii0xODUuODYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTIwLjU0IiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjY1Mi40NSIgeDE9IjY1Mi40NSI+PHN0b3Agc3RvcC1jb2xvcj0iIzA1QjVEQyIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzA0QjBENyIgb2Zmc2V0PSIuMjE5NyIvPjxzdG9wIHN0b3AtY29sb3I9IiMwNUE0QzkiIG9mZnNldD0iLjUzNzEiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDU5MUI0IiBvZmZzZXQ9Ii45MTIyIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzA1OENBRSIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJtNTQyLjMgMjY3LjRjMS41LTI5LjQgNy41LTY1LjYgMTMuMi0xMDAuNiA5LjgtNjAuMSAxOS4zLTExMi44IDEzLjYtMTY2LjhoLTcwLjhjLTEuNCAxMS40LTIuOSAxOS4yLTEuOCA0MS44IDEuNSAzMS42IDcuNSA3MC41IDEzLjIgMTA4LjIgOC40IDU1LjQgMTYuNiAxMDguOCAxNS4xIDE1Ni40aDE5LjJjLTEuMy02LjItMi44LTE3LjktMS43LTM5eiIgY2xhc3M9InN0MTUiIGZpbGw9InVybCgjU1ZHSURfMTBfKSIvPjxwb2x5Z29uIHBvaW50cz0iMzc1LjcgMTUzLjIgMzU4LjEgMCAzNTguMSAzMDYuNCIgZmlsbD0iIzJhMzg4NiIgY2xhc3M9InN0MTYiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iU1ZHSURfMTFfIiB5Mj0iNzcuMTM2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9Ii00LjMyODEiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgLTExOC45OCAxMjAuNTQpIiB4Mj0iNzk2LjcxIiB4MT0iNzUxLjA1Ij48c3RvcCBzdG9wLWNvbG9yPSIjNjJCMTZFIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjODdCOTU3IiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxwYXRoIGQ9Im02NDEuNiA1MC42YzEuNyAyNy4zIDEwIDU4LjcgMTguOCA5MiAxLjQgNS4zIDIuOCAxMC43IDQuMiAxNi4yIDEzLjUtNDguNCAyNi42LTEwMy41IDE1LjktMTU4LjhoLTMwYy03LjUgMTMuNi0xMC4zIDI5LjQtOC45IDUwLjZ6IiBjbGFzcz0ic3QxNyIgZmlsbD0idXJsKCNTVkdJRF8xMV8pIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzEyXyIgeTI9Ii0xODkuMjgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iMTEzLjcxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIC0xMTguOTggMTIwLjU0KSIgeDI9IjYzMS41OSIgeDE9IjU1MC40Ij48c3RvcCBzdG9wLWNvbG9yPSIjMDY5QUQ0IiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjMzBBMENFIiBvZmZzZXQ9Ii4zNTI1Ii8+PHN0b3Agc3RvcC1jb2xvcj0iIzVCQjBDMCIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJtNTA5LjggMTUwYy01LjctMzcuNy0xMS43LTc2LjYtMTMuMi0xMDguMi0xLjEtMjIuNyAwLjQtMzAuNCAxLjgtNDEuOGgtNDEuNWMxLjUgNDAuMS0xLjUgODUuMy03IDE2MC44LTMuMSA0My41LTggMTEwLjUtNyAxNDUuN2g4Mi4xYzEuNC00Ny43LTYuOC0xMDEuMS0xNS4yLTE1Ni41eiIgY2xhc3M9InN0MTgiIGZpbGw9InVybCgjU1ZHSURfMTJfKSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8xM18iIHkyPSItMTg1Ljg2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeTE9IjEyMC41NCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMTE4Ljk4IDEyMC41NCkiIHgyPSI1MDUuMzMiIHgxPSI1MDUuMzMiPjxzdG9wIHN0b3AtY29sb3I9IiMxRTQ1OEUiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiMxRjRGOTYiIG9mZnNldD0iLjI0MTEiLz48c3RvcCBzdG9wLWNvbG9yPSIjMkI2QUFCIiBvZmZzZXQ9Ii43MjkyIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzMzN0JCOSIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48cG9seWdvbiBwb2ludHM9IjM1OC4xIDMwNi40IDQxNC42IDMwNi40IDQxNC42IDAgMzU4LjEgMCAzNzUuNyAxNTMuMiIgZmlsbD0idXJsKCNTVkdJRF8xM18pIiBjbGFzcz0ic3QxOSIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8xNF8iIHkyPSIxMjAuNTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5MT0iLTE4NS44NiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMTE4Ljk4IDEyMC41NCkiIHgyPSI1NTQuOTIiIHgxPSI1NTQuOTIiPjxzdG9wIHN0b3AtY29sb3I9IiMzRjlBQzkiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiMyMDYyQTIiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0ibTQ0OS45IDE2MC44YzUuNS03NS41IDguNS0xMjAuNiA3LTE2MC44aC00Mi4ybC0wLjEgMzA2LjRoMjguM2MtMS0zNS4xIDMuOC0xMDIuMSA3LTE0NS42eiIgY2xhc3M9InN0MjAiIGZpbGw9InVybCgjU1ZHSURfMTRfKSIvPjwvZz4KPC9zdmc+Cg==);
-}
-
-body:after {
- position: absolute;
- right: 25px;
- top: 36px;
- width: 135px;
- height: 48px;
- content: '';
- background-repeat: no-repeat;
- background-size: cover;
- background-image: url(/static/img/bosch-logo.svg);
-}
-
-.swagger-ui {
- font-family: "Bosch Sans", sans-serif;
-}
-
-/*custom docs*/
-.docs {
- position: relative;
- font-size: 14px;
-}
-
-.docs > summary {
- position: absolute;
- right: 0;
- top: -25px;
- cursor: pointer;
-}
-
-.docs-open:hover {
- text-decoration: underline;
-}
-
-/*Remove topbar*/
-.swagger-ui .topbar {
- display: none
-}
-
-/*Remove models view*/
-.swagger-ui .models {
- display: none;
-}
-
-/*Remove application/json select*/
-.swagger-ui .opblock .opblock-section-header > label, .swagger-ui .response-controls {
- display: none;
-}
-
-/*Remove border radius*/
-.swagger-ui .opblock, .swagger-ui .opblock .opblock-summary-method, .swagger-ui select {
- border-radius: 0;
- box-shadow: none;
-}
-
-/*remove links in response*/
-.swagger-ui .response-col_links {
- display: none;
-}
-
-/*remove version*/
-.swagger-ui .info .title span {
- display: none;
-}
-
-/*separator before methods*/
-.swagger-ui .scheme-container {
- box-shadow: none;
- border-bottom: 1px solid var(--light-grey);
-}
-
-/*tag separator*/
-.swagger-ui .opblock-tag {
- border-bottom: 1px solid var(--light-grey);
-}
-
-/*parameters/responses bar*/
-.swagger-ui .opblock .opblock-section-header {
- box-shadow: none;
- background: #fff;
-}
-
-/*select*/
-.swagger-ui select {
- background-color: var(--light-grey-w75);
- border: none;
- height: 36px;
-}
-
-/*button*/
-.swagger-ui .btn {
- border-radius: 0;
- box-shadow: none;
-}
-
-.swagger-ui .btn:hover {
- box-shadow: none;
-}
-
-/*authorize button */
-.swagger-ui .btn.authorize {
- color: var(--light-green);
- border-color: var(--light-green);
-}
-
-.swagger-ui .btn.authorize svg {
- fill: var(--light-green);
-}
-
-/*auth inputs*/
-.swagger-ui .auth-container input[type="password"], .swagger-ui .auth-container input[type="text"] {
- border-radius: 0;
- box-shadow: none;
- border-color: var(--light-grey);
-}
-
-.swagger-ui .dialog-ux .modal-ux {
- border-radius: 0;
-}
-
-/*cancel button*/
-.swagger-ui .btn.cancel {
- color: var(--red);
- border-color: var(--red);
-}
-
-/*download button*/
-.swagger-ui .download-contents {
- border-radius: 0;
- height: 28px;
- width: 80px;
-}
-
-/*model*/
-.swagger-ui .model-box {
- border-radius: 0;
-}
-
-/*execute button*/
-.swagger-ui .btn.execute {
- background-color: var(--dark-blue);
- border-color: var(--dark-blue);
- height: 30px;
- line-height: 0.7;
-}
-
-.swagger-ui .btn-group .btn:last-child {
- border-radius: 0;
- height: 30px;
- border-color: var(--dark-blue);
-}
-
-.swagger-ui .btn-group .btn:first-child {
- border-radius: 0;
-}
-
-.swagger-ui .btn-group {
- padding: 0 20px;
-}
-
-/*parameter input*/
-.swagger-ui .parameters-col_description input[type="text"] {
- border-radius: 0;
-}
-
-/*required label*/
-.swagger-ui .parameter__name.required > span {
- color: var(--red) !important;
-}
-
-.swagger-ui .parameter__name.required::after {
- color: var(--red);
-}
-/*Remove colored parameters bar*/
-.swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after {
- background: none;
-}
-
-/*code*/
-.swagger-ui .opblock-body pre.microlight {
- border-radius: 0;
-}
-
-.swagger-ui .highlight-code > .microlight {
- min-height: 0;
-}
-
-/*request body*/
-.swagger-ui textarea {
- border-radius: 0;
-}
-
-/*parameters smaller padding*/
-.swagger-ui .execute-wrapper {
- padding-top: 0;
- padding-bottom: 0;
-}
-
-.swagger-ui .btn.execute {
- margin-bottom: 20px;
-}
-
-.swagger-ui .opblock-description-wrapper {
- margin-top: 20px;
-}
-
-.swagger-ui .opblock-description-wrapper {
- margin-top: 5px;
-}
-
-.opblock-section .opblock-section-request-body > div > div {
- padding-top: 18px;
-}
-
-/*response element positions*/
-.swagger-ui .model-example {
- position: relative;
- margin-top: 0;
-}
-
-.swagger-ui .tab {
- position: absolute;
- top: -35px;
- right: 0;
-}
-
-.swagger-ui table tbody tr td {
- padding: 0;
-}
-
-.swagger-ui .renderedMarkdown p {
- margin: 8px auto;
-}
-
-/*Method colors*/
-.swagger-ui .opblock.opblock-get .opblock-summary-method {
- background: var(--dark-blue);
-}
-
-.swagger-ui .opblock.opblock-get .opblock-summary {
- border-color: var(--dark-blue);
-}
-
-.swagger-ui .opblock.opblock-get {
- background: var(--dark-blue-w75);
- border-color: var(--dark-blue);
-}
-
-.swagger-ui .opblock.opblock-post .opblock-summary-method {
- background: var(--dark-green);
-}
-
-.swagger-ui .opblock.opblock-post .opblock-summary {
- border-color: var(--dark-green);
-}
-
-.swagger-ui .opblock.opblock-post {
- background: var(--dark-green-w75);
- border-color: var(--dark-green);
-}
-
-.swagger-ui .opblock.opblock-put .opblock-summary-method {
- background: var(--turquoise);
-}
-
-.swagger-ui .opblock.opblock-put .opblock-summary {
- border-color: var(--turquoise);
-}
-
-.swagger-ui .opblock.opblock-put {
- background: var(--turquoise-w75);
- border-color: var(--turquoise);
-}
-
-.swagger-ui .opblock.opblock-delete .opblock-summary-method {
- background: var(--fuchsia);
-}
-
-.swagger-ui .opblock.opblock-delete .opblock-summary {
- border-color: var(--fuchsia);
-}
-
-.swagger-ui .opblock.opblock-delete {
- background: var(--fuchsia-w75);
- border-color: var(--fuchsia);
-}
\ No newline at end of file