implemented /help route
This commit is contained in:
parent
364ad1964e
commit
6ae49e9f09
@ -65,6 +65,7 @@ tags:
|
|||||||
- name: /template
|
- name: /template
|
||||||
- name: /model
|
- name: /model
|
||||||
- name: /user
|
- name: /user
|
||||||
|
- name: /help
|
||||||
|
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
@ -76,6 +77,7 @@ paths:
|
|||||||
- $ref: 'template.yaml'
|
- $ref: 'template.yaml'
|
||||||
- $ref: 'model.yaml'
|
- $ref: 'model.yaml'
|
||||||
- $ref: 'user.yaml'
|
- $ref: 'user.yaml'
|
||||||
|
- $ref: 'help.yaml'
|
||||||
|
|
||||||
|
|
||||||
components:
|
components:
|
||||||
|
60
api/help.yaml
Normal file
60
api/help.yaml
Normal file
@ -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 <br> 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'
|
@ -47,3 +47,12 @@ Group:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: vn
|
example: vn
|
||||||
|
|
||||||
|
Key:
|
||||||
|
name: key
|
||||||
|
description: URIComponent encoded string
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: '%2Fdocumentation%2Fdatabase'
|
@ -233,3 +233,13 @@ ModelItem:
|
|||||||
label:
|
label:
|
||||||
type: string
|
type: string
|
||||||
example: 'ml/g'
|
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
|
@ -114,6 +114,7 @@ app.use('/', require('./routes/measurement'));
|
|||||||
app.use('/', require('./routes/template'));
|
app.use('/', require('./routes/template'));
|
||||||
app.use('/', require('./routes/model'));
|
app.use('/', require('./routes/model'));
|
||||||
app.use('/', require('./routes/user'));
|
app.use('/', require('./routes/user'));
|
||||||
|
app.use('/', require('./routes/help'));
|
||||||
|
|
||||||
// static files
|
// static files
|
||||||
app.use('/static', express.static('static'));
|
app.use('/static', express.static('static'));
|
||||||
|
16
src/models/help.ts
Normal file
16
src/models/help.ts
Normal file
@ -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 <Q extends mongoose.DocumentQuery<any, any>> (req) {
|
||||||
|
db.log(req, this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default mongoose.model<any, mongoose.Model<any, any>>('help', HelpSchema);
|
184
src/routes/help.spec.ts
Normal file
184
src/routes/help.spec.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
55
src/routes/help.ts
Normal file
55
src/routes/help.ts
Normal file
@ -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;
|
34
src/routes/validate/help.ts
Normal file
34
src/routes/validate/help.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -893,6 +893,26 @@
|
|||||||
"user_id" : {"$oid": "000000000000000000000003"},
|
"user_id" : {"$oid": "000000000000000000000003"},
|
||||||
"__v" : 0
|
"__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"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user