From 15bc11dfa6c566974c776489deac55437a08a6f2 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Tue, 14 Sep 2021 15:39:55 +0200 Subject: [PATCH 1/4] feat: ui hooks extension --- lib/mode/dynamic.js | 9 +++- lib/mode/static.js | 3 +- lib/routes.js | 17 +++++++ package.json | 1 + test/hooks.js | 108 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 test/hooks.js diff --git a/lib/mode/dynamic.js b/lib/mode/dynamic.js index ebee051e..6c4b42b5 100644 --- a/lib/mode/dynamic.js +++ b/lib/mode/dynamic.js @@ -29,7 +29,14 @@ module.exports = function (fastify, opts, done) { const initOAuth = opts.initOAuth || {} const staticCSP = opts.staticCSP const transformStaticCSP = opts.transformStaticCSP - fastify.register(require('../routes'), { prefix, uiConfig, initOAuth, staticCSP, transformStaticCSP }) + fastify.register(require('../routes'), { + prefix, + uiConfig, + initOAuth, + staticCSP, + transformStaticCSP, + hooks: opts.uiHooks + }) } const cache = { diff --git a/lib/mode/static.js b/lib/mode/static.js index d9097717..30e0b497 100644 --- a/lib/mode/static.js +++ b/lib/mode/static.js @@ -65,7 +65,8 @@ module.exports = function (fastify, opts, done) { initOAuth: opts.initOAuth || {}, baseDir: opts.specification.baseDir, staticCSP: opts.staticCSP, - transformStaticCSP: opts.transformStaticCSP + transformStaticCSP: opts.transformStaticCSP, + hooks: opts.uiHooks } fastify.register(require('../routes'), options) diff --git a/lib/routes.js b/lib/routes.js index 1725e708..521093d8 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -51,10 +51,22 @@ function fastifySwagger (fastify, opts, done) { }) } + const hooks = Object.create(null) + if (opts.hooks) { + const additionalHooks = [ + 'onRequest', + 'preHandler' + ] + for (const hook of additionalHooks) { + hooks[hook] = opts.hooks[hook] + } + } + fastify.route({ url: '/', method: 'GET', schema: { hide: true }, + ...hooks, handler: (req, reply) => { reply.redirect(getRedirectPathForTheRootRoute(req.raw.url)) } @@ -64,6 +76,7 @@ function fastifySwagger (fastify, opts, done) { url: '/uiConfig', method: 'GET', schema: { hide: true }, + ...hooks, handler: (req, reply) => { reply.send(opts.uiConfig) } @@ -73,6 +86,7 @@ function fastifySwagger (fastify, opts, done) { url: '/initOAuth', method: 'GET', schema: { hide: true }, + ...hooks, handler: (req, reply) => { reply.send(opts.initOAuth) } @@ -82,6 +96,7 @@ function fastifySwagger (fastify, opts, done) { url: '/json', method: 'GET', schema: { hide: true }, + ...hooks, handler: function (req, reply) { reply.send(fastify.swagger()) } @@ -91,6 +106,7 @@ function fastifySwagger (fastify, opts, done) { url: '/yaml', method: 'GET', schema: { hide: true }, + ...hooks, handler: function (req, reply) { reply .type('application/x-yaml') @@ -115,6 +131,7 @@ function fastifySwagger (fastify, opts, done) { url: '/*', method: 'GET', schema: { hide: true }, + ...hooks, handler: function (req, reply) { const file = req.params['*'] reply.sendFile(file) diff --git a/package.json b/package.json index bd8efa80..a58047e3 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "devDependencies": { "@types/node": "^16.0.0", "fastify": "^3.7.0", + "fastify-basic-auth": "^2.1.0", "fastify-helmet": "^5.0.3", "fs-extra": "^10.0.0", "joi": "^14.3.1", diff --git a/test/hooks.js b/test/hooks.js new file mode 100644 index 00000000..9ef63638 --- /dev/null +++ b/test/hooks.js @@ -0,0 +1,108 @@ +'use strict' + +const { test } = require('tap') +const Fastify = require('fastify') +const yaml = require('js-yaml') + +const fastifySwagger = require('../index') +const { swaggerOption, schemaBody } = require('../examples/options') + +const authOptions = { + validate (username, password, req, reply, done) { + if (username === 'admin' && password === 'admin') { + done() + } else { + done(new Error('Winter is coming')) + } + }, + authenticate: true +} + +function basicAuthEncode (username, password) { + return 'Basic ' + Buffer.from(username + ':' + password).toString('base64') +} + +test('hooks on static swagger', async t => { + const fastify = Fastify() + await fastify.register(require('fastify-basic-auth'), authOptions) + fastify.register(fastifySwagger, { + mode: 'static', + specification: { + path: './examples/example-static-specification.yaml' + }, + exposeRoute: true, + uiHooks: { + onRequest: fastify.basicAuth + } + }) + + let res = await fastify.inject('/documentation') + t.equal(res.statusCode, 401, 'root auth required') + + res = await fastify.inject('/documentation/yaml') + t.equal(res.statusCode, 401, 'auth required yaml') + res = await fastify.inject({ + method: 'GET', + url: '/documentation/yaml', + headers: { authorization: basicAuthEncode('admin', 'admin') } + }) + t.equal(res.statusCode, 200) + t.equal(res.headers['content-type'], 'application/x-yaml') + try { + yaml.load(res.payload) + t.pass('valid swagger yaml') + } catch (err) { + t.fail(err) + } + + res = await fastify.inject('/documentation/json') + t.equal(res.statusCode, 401, 'auth required json') + res = await fastify.inject({ + method: 'GET', + url: '/documentation/json', + headers: { authorization: basicAuthEncode('admin', 'admin') } + }) + t.equal(typeof res.payload, 'string') + t.equal(res.headers['content-type'], 'application/json; charset=utf-8') + try { + yaml.load(res.payload) + t.pass('valid swagger json') + } catch (err) { + t.fail(err) + } +}) + +test('hooks on dynamic swagger', async t => { + const fastify = Fastify() + await fastify.register(require('fastify-basic-auth'), authOptions) + + fastify.register(fastifySwagger, { + ...swaggerOption, + exposeRoute: true, + uiHooks: { + onRequest: fastify.basicAuth + } + }) + + fastify.get('/fooBar123', schemaBody, () => {}) + + let res = await fastify.inject('/documentation') + t.equal(res.statusCode, 401, 'root auth required') + + res = await fastify.inject('/documentation/yaml') + t.equal(res.statusCode, 401, 'auth required yaml') + + res = await fastify.inject('/documentation/json') + t.equal(res.statusCode, 401, 'auth required json') + res = await fastify.inject({ + method: 'GET', + url: '/documentation/json', + headers: { authorization: basicAuthEncode('admin', 'admin') } + }) + t.equal(typeof res.payload, 'string') + t.equal(res.headers['content-type'], 'application/json; charset=utf-8') + + const swaggerObject = res.json() + t.ok(swaggerObject.paths) + t.ok(swaggerObject.paths['/fooBar123']) +}) From fe5cbc94d459c3936924f19bceee66c6ed68c595 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Tue, 14 Sep 2021 15:58:50 +0200 Subject: [PATCH 2/4] add docs --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index f3a3a406..68436d26 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ fastify.register(require('fastify-swagger'), { docExpansion: 'full', deepLinking: false }, + uiHooks: { + onRequest: function (request, reply, next) { next() }, + preHandler: function (request, reply, next) { next() } + }, staticCSP: true, transformStaticCSP: (header) => header, exposeRoute: true @@ -225,6 +229,7 @@ An example of using `fastify-swagger` with `static` mode enabled can be found [h | transform | null | Transform method for schema. | | transformStaticCSP | undefined | Synchronous function to transform CSP header for static resources if the header has been previously set. | | uiConfig | {} | Configuration options for [Swagger UI](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md). Must be literal values, see [#5710](https://github.com/swagger-api/swagger-ui/issues/5710).| + | uiHooks | {} | Additional hooks for the documentation's routes. You can provide the `onRequest` and `preHandler` hooks with the same [route's options](https://www.fastify.io/docs/latest/Routes/#options) interface.| | refResolver | {} | Option to manage the `$ref`s of your application's schemas. Read the [`$ref` documentation](#register.options.refResolver) | If you set `exposeRoute` to `true` the plugin will expose the documentation with the following APIs: @@ -642,6 +647,32 @@ There are two ways to hide a route from the Swagger UI: - Pass `{ hide: true }` to the schema object inside the route declaration. - Use the tag declared in `hiddenTag` options property inside the route declaration. Default is `X-HIDDEN`. + +#### Protect your documentation routes + +You can protect your documentation configuring an authentication hook. +Here is an example with the [`fastify-basic-auth`](https://github.com/fastify/fastify-basic-auth) plugin: + +```js +await fastify.register(require('fastify-basic-auth'), { + validate (username, password, req, reply, done) { + if (username === 'admin' && password === 'admin') { + done() + } else { + done(new Error('You can not access')) + } + }, + authenticate: true +}) + +fastify.register(fastifySwagger, { + exposeRoute: true, + uiHooks: { + onRequest: fastify.basicAuth + } +}) +``` + ### Swagger function options From 8d7e6088f9496533ca4c320091d31c3b7ce7de19 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Tue, 14 Sep 2021 19:29:40 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Vincent LE GOFF --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 68436d26..fbb3c180 100644 --- a/README.md +++ b/README.md @@ -650,8 +650,8 @@ There are two ways to hide a route from the Swagger UI: #### Protect your documentation routes -You can protect your documentation configuring an authentication hook. -Here is an example with the [`fastify-basic-auth`](https://github.com/fastify/fastify-basic-auth) plugin: +You can protect your documentation by configuring an authentication hook. +Here is an example using [`fastify-basic-auth`](https://github.com/fastify/fastify-basic-auth) plugin: ```js await fastify.register(require('fastify-basic-auth'), { From 94e43e873bd68f01c53a5fb84a8c523b8bd1ff8f Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Wed, 15 Sep 2021 10:37:46 +0200 Subject: [PATCH 4/4] Update README.md Co-authored-by: Frazer Smith --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fbb3c180..4a75d40d 100644 --- a/README.md +++ b/README.md @@ -651,7 +651,7 @@ There are two ways to hide a route from the Swagger UI: #### Protect your documentation routes You can protect your documentation by configuring an authentication hook. -Here is an example using [`fastify-basic-auth`](https://github.com/fastify/fastify-basic-auth) plugin: +Here is an example using the [`fastify-basic-auth`](https://github.com/fastify/fastify-basic-auth) plugin: ```js await fastify.register(require('fastify-basic-auth'), {