diff --git a/api/api.yaml b/api/api.yaml
index d85acc7..5d0ab02 100644
--- a/api/api.yaml
+++ b/api/api.yaml
@@ -65,6 +65,7 @@ tags:
- name: /template
- name: /model
- name: /user
+ - name: /help
paths:
@@ -76,6 +77,7 @@ paths:
- $ref: 'template.yaml'
- $ref: 'model.yaml'
- $ref: 'user.yaml'
+ - $ref: 'help.yaml'
components:
diff --git a/api/help.yaml b/api/help.yaml
new file mode 100644
index 0000000..de36cbe
--- /dev/null
+++ b/api/help.yaml
@@ -0,0 +1,60 @@
+/help/{key}:
+ parameters:
+ - $ref: 'api.yaml#/components/parameters/Key'
+ get:
+ summary: get help text for key
+ description: 'Auth: basic, levels: predict, read, write, dev, admin, depending on the set document level'
+ tags:
+ - /help
+ responses:
+ 200:
+ description: the required text
+ content:
+ application/json:
+ schema:
+ $ref: 'api.yaml#/components/schemas/Help'
+ 403:
+ $ref: 'api.yaml#/components/responses/403'
+ 404:
+ $ref: 'api.yaml#/components/responses/404'
+ 500:
+ $ref: 'api.yaml#/components/responses/500'
+ post:
+ summary: add/replace help text
+ description: 'Auth: basic, levels: dev, admin
If the given key exists, the item is replaced,
+ otherwise it is newly created'
+ tags:
+ - /help
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: 'api.yaml#/components/schemas/Help'
+ 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'
+ 500:
+ $ref: 'api.yaml#/components/responses/500'
+ delete:
+ summary: remove help text
+ description: 'Auth: basic, levels: dev, admin'
+ tags:
+ - /help
+ 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'
\ No newline at end of file
diff --git a/api/parameters.yaml b/api/parameters.yaml
index 192b15a..a17127f 100644
--- a/api/parameters.yaml
+++ b/api/parameters.yaml
@@ -46,4 +46,13 @@ Group:
required: true
schema:
type: string
- example: vn
\ No newline at end of file
+ example: vn
+
+Key:
+ name: key
+ description: URIComponent encoded string
+ in: path
+ required: true
+ schema:
+ type: string
+ example: '%2Fdocumentation%2Fdatabase'
\ No newline at end of file
diff --git a/api/schemas.yaml b/api/schemas.yaml
index a61a819..1da048f 100644
--- a/api/schemas.yaml
+++ b/api/schemas.yaml
@@ -232,4 +232,14 @@ ModelItem:
example: https://definma-model-test.apps.de1.bosch-iot-cloud.com/predict/model1-1
label:
type: string
- example: 'ml/g'
\ No newline at end of file
+ example: 'ml/g'
+
+Help:
+ properties:
+ text:
+ type: string
+ example: This page documents the database.
+ level:
+ type: string
+ description: can be also null to allow access without authorization
+ example: read
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index a1c7417..a2165e9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -114,6 +114,7 @@ app.use('/', require('./routes/measurement'));
app.use('/', require('./routes/template'));
app.use('/', require('./routes/model'));
app.use('/', require('./routes/user'));
+app.use('/', require('./routes/help'));
// static files
app.use('/static', express.static('static'));
diff --git a/src/models/help.ts b/src/models/help.ts
new file mode 100644
index 0000000..a04ae33
--- /dev/null
+++ b/src/models/help.ts
@@ -0,0 +1,16 @@
+import db from '../db';
+import mongoose from 'mongoose';
+
+const HelpSchema = new mongoose.Schema({
+ key: {type: String, index: {unique: true}},
+ level: String,
+ text: String
+}, {minimize: false});
+
+// changelog query helper
+HelpSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('help', HelpSchema);
\ No newline at end of file
diff --git a/src/routes/help.spec.ts b/src/routes/help.spec.ts
new file mode 100644
index 0000000..590d47b
--- /dev/null
+++ b/src/routes/help.spec.ts
@@ -0,0 +1,184 @@
+import should from 'should/as-function';
+import TestHelper from "../test/helper";
+import HelpModel from '../models/help';
+
+
+describe('/help', () => {
+ let server;
+ before(done => TestHelper.before(done));
+ beforeEach(done => server = TestHelper.beforeEach(server, done));
+ afterEach(done => TestHelper.afterEach(server, done));
+ after(done => TestHelper.after(done));
+
+ describe('GET /help/{key}', () => {
+ it('returns the required text', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/help/%2Fsamples',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ res: {text: 'Samples help', level: 'read'}
+ });
+ });
+ it('returns the required text without authorization if allowed', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/help/%2Fdocumentation',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ res: {text: 'Documentation help', level: 'none'}
+ });
+ });
+ it('returns 404 for an invalid key', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/help/documentation/database',
+ httpStatus: 404
+ });
+ });
+ it('returns 404 for an unknown key', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/help/xxx',
+ auth: {basic: 'janedoe'},
+ httpStatus: 404
+ });
+ });
+ it('returns 403 for a text with a higher level than given', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/help/%2Fmodels',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403
+ });
+ });
+ it('rejects an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/help/%2Fsamples',
+ auth: {api: 'janedoe'},
+ httpStatus: 401,
+ });
+ });
+ it('rejects an unauthorized request if a level is given', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/help/%2Fsamples',
+ httpStatus: 401
+ });
+ });
+ });
+
+ describe('POST /help/{key}', () => {
+ it('changes the required text', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/help/%2Fsamples',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {text: 'New samples help', level: 'write'}
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).be.eql({status: 'OK'});
+ HelpModel.find({key: '/samples'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.lengthOf(1);
+ should(data[0]).have.only.keys('_id', 'key', 'text', 'level');
+ should(data[0]).property('key', '/samples');
+ should(data[0]).property('text', 'New samples help');
+ should(data[0]).property('level', 'write');
+ done();
+ });
+ });
+ });
+ it('saves a new text', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/help/%2Fmaterials',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {text: 'Materials help', level: 'dev'}
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).be.eql({status: 'OK'});
+ HelpModel.find({key: '/materials'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.lengthOf(1);
+ should(data[0]).have.only.keys('_id', 'key', 'text', 'level', '__v');
+ should(data[0]).property('key', '/materials');
+ should(data[0]).property('text', 'Materials help');
+ should(data[0]).property('level', 'dev');
+ done();
+ });
+ });
+ });
+ it('rejects an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/help/%2Fsamples',
+ auth: {key: 'admin'},
+ httpStatus: 401,
+ req: {text: 'New samples help', level: 'write'}
+ });
+ });
+ it('rejects a write user', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/help/%2Fsamples',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403,
+ req: {text: 'New samples help', level: 'write'}
+ });
+ });
+ it('rejects an unauthorized request', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/help/%2Fsamples',
+ httpStatus: 401,
+ req: {text: 'New samples help', level: 'write'}
+ });
+ });
+ });
+
+ describe('DELETE /help/{key}', () => {
+ it('deletes the required entry', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/help/%2Fsamples',
+ auth: {basic: 'admin'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).be.eql({status: 'OK'});
+ HelpModel.find({key: '/materials'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.lengthOf(0);
+ done();
+ });
+ });
+ });
+ it('rejects an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/help/%2Fsamples',
+ auth: {key: 'admin'},
+ httpStatus: 401
+ });
+ });
+ it('rejects a write user', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/help/%2Fsamples',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403
+ });
+ });
+ it('rejects an unauthorized request', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/help/%2Fsamples',
+ httpStatus: 401
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/routes/help.ts b/src/routes/help.ts
new file mode 100644
index 0000000..3a2cb51
--- /dev/null
+++ b/src/routes/help.ts
@@ -0,0 +1,55 @@
+import express from 'express';
+import HelpModel from '../models/help';
+import HelpValidate from './validate/help';
+import res400 from './validate/res400';
+import globals from '../globals';
+
+const router = express.Router();
+
+router.get('/help/:key', (req, res, next) => {
+ const {error: paramError, value: key} = HelpValidate.params(req.params);
+ if (paramError) return res400(paramError, res);
+
+ HelpModel.findOne(key).lean().exec((err, data) => {
+ if (err) return next(err);
+
+ if (!data) {
+ return res.status(404).json({status: 'Not found'});
+ }
+ if (data.level !== 'none') { // check level
+ if (!req.auth(res,
+ Object.values(globals.levels).slice(Object.values(globals.levels).findIndex(e => e === data.level))
+ , 'basic')) return;
+ }
+ res.json(HelpValidate.output(data));
+ })
+});
+
+router.post('/help/:key', (req, res, next) => {
+ if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
+ const {error: paramError, value: key} = HelpValidate.params(req.params);
+ if (paramError) return res400(paramError, res);
+ const {error, value: help} = HelpValidate.input(req.body);
+ if (error) return res400(error, res);
+
+ HelpModel.findOneAndUpdate(key, help, {upsert: true}).log(req).lean().exec(err => {
+ if (err) return next(err);
+ res.json({status: 'OK'});
+ });
+});
+
+router.delete('/help/:key', (req, res, next) => {
+ if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
+ const {error: paramError, value: key} = HelpValidate.params(req.params);
+ if (paramError) return res400(paramError, res);
+
+ HelpModel.findOneAndDelete(key).log(req).lean().exec((err, data) => {
+ if (err) return next(err);
+ if (!data) {
+ return res.status(404).json({status: 'Not found'});
+ }
+ res.json({status: 'OK'});
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/routes/validate/help.ts b/src/routes/validate/help.ts
new file mode 100644
index 0000000..6dcf64a
--- /dev/null
+++ b/src/routes/validate/help.ts
@@ -0,0 +1,34 @@
+import Joi from 'joi';
+import globals from '../../globals';
+
+export default class HelpValidate {
+ private static help = {
+ text: Joi.string()
+ .allow('')
+ .max(8192),
+
+ level: Joi.string()
+ .valid('none', ...Object.values(globals.levels))
+ }
+
+ static input (data) {
+ return Joi.object({
+ text: this.help.text.required(),
+ level: this.help.level.required()
+ }).validate(data);
+ }
+
+ static output (data) {
+ const {value, error} = Joi.object({
+ text: this.help.text,
+ level: this.help.level
+ }).validate(data, {stripUnknown: true});
+ return error !== undefined? null : value;
+ }
+
+ static params(data) {
+ return Joi.object({
+ key: Joi.string().min(1).max(128)
+ }).validate(data);
+ }
+}
\ No newline at end of file
diff --git a/src/test/db.json b/src/test/db.json
index f6118d2..ea276da 100644
--- a/src/test/db.json
+++ b/src/test/db.json
@@ -893,6 +893,26 @@
"user_id" : {"$oid": "000000000000000000000003"},
"__v" : 0
}
+ ],
+ "helps": [
+ {
+ "_id": {"$oid":"150000000000000000000001"},
+ "key": "/documentation",
+ "text": "Documentation help",
+ "level": "none"
+ },
+ {
+ "_id": {"$oid":"150000000000000000000002"},
+ "key": "/samples",
+ "text": "Samples help",
+ "level": "read"
+ },
+ {
+ "_id": {"$oid":"150000000000000000000003"},
+ "key": "/models",
+ "text": "Models help",
+ "level": "dev"
+ }
]
}
}
\ No newline at end of file