diff --git a/README.md b/README.md index 6e1af48..a28d686 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ await fastify.ready() | baseDir | undefined | Specify the directory where all spec files that are included in the main one using $ref will be located. By default, this is the directory where the main spec file is located. Provided value should be an absolute path without trailing slash. | | initOAuth | {} | Configuration options for [Swagger UI initOAuth](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). | | routePrefix | '/documentation' | Overwrite the default Swagger UI route prefix. | + | noRedirect | false | Switches from redirection to `static/index.html` to serving the UI directly at the route prefix for a cleaner url. | staticCSP | false | Enable CSP header for static resources. | | transformStaticCSP | undefined | Synchronous function to transform CSP header for static resources if the header has been previously set. | | transformSpecification | undefined | Synchronous function to transform the swagger document. | diff --git a/index.js b/index.js index b0435ea..363110d 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,7 @@ async function fastifySwaggerUi (fastify, opts) { hooks: opts.uiHooks, theme: opts.theme || {}, logo: logoContent, + noRedirect: opts.noRedirect, ...opts }) } diff --git a/lib/index-html.js b/lib/index-html.js index 518c4a6..ab8d9c0 100644 --- a/lib/index-html.js +++ b/lib/index-html.js @@ -1,29 +1,30 @@ 'use strict' function indexHtml (opts) { + const staticPrefix = opts.noRedirect ? opts.staticPrefix : '' return ` ${opts.theme?.title || 'Swagger UI'} - - - ${opts.theme && opts.theme.css ? opts.theme.css.map(css => `\n`).join('') : ''} + + + ${opts.theme && opts.theme.css ? opts.theme.css.map(css => `\n`).join('') : ''} ${opts.theme && opts.theme.favicon -? opts.theme.favicon.map(favicon => `\n`).join('') +? opts.theme.favicon.map(favicon => `\n`).join('') : ` - - + + `}
- - - - ${opts.theme && opts.theme.js ? opts.theme.js.map(js => `\n`).join('') : ''} + + + + ${opts.theme && opts.theme.js ? opts.theme.js.map(js => `\n`).join('') : ''} ` diff --git a/lib/routes.js b/lib/routes.js index a179ff8..73748fd 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -66,15 +66,43 @@ function fastifySwagger (fastify, opts, done) { } } - fastify.route({ - url: '/', - method: 'GET', - schema: { hide: true }, - ...hooks, - handler: (req, reply) => { - reply.redirect(getRedirectPathForTheRootRoute(req.raw.url)) - } - }) + const indexHtmlContent = indexHtml({ ...opts, staticPrefix }) + + if (opts.noRedirect) { + fastify.route({ + url: '/', + method: 'GET', + schema: { hide: true }, + ...hooks, + handler: (req, reply) => { + reply + .header('content-type', 'text/html; charset=utf-8') + .send(indexHtmlContent) + } + }) + } else { + fastify.route({ + url: '/', + method: 'GET', + schema: { hide: true }, + ...hooks, + handler: (req, reply) => { + reply.redirect(getRedirectPathForTheRootRoute(req.raw.url)) + } + }) + + fastify.route({ + url: `${staticPrefix}/index.html`, + method: 'GET', + schema: { hide: true }, + ...hooks, + handler: (req, reply) => { + reply + .header('content-type', 'text/html; charset=utf-8') + .send(indexHtmlContent) + } + }) + } if (opts.theme) { const themePrefix = `${staticPrefix}/theme` @@ -127,20 +155,6 @@ function fastifySwagger (fastify, opts, done) { } } - const indexHtmlContent = indexHtml(opts) - - fastify.route({ - url: `${staticPrefix}/index.html`, - method: 'GET', - schema: { hide: true }, - ...hooks, - handler: (req, reply) => { - reply - .header('content-type', 'text/html; charset=utf-8') - .send(indexHtmlContent) - } - }) - const swaggerInitializerContent = swaggerInitializer(opts) fastify.route({ diff --git a/test/route.test.js b/test/route.test.js index 56e41aa..0ddc44b 100644 --- a/test/route.test.js +++ b/test/route.test.js @@ -122,6 +122,29 @@ test('/documentation should redirect to ./documentation/static/index.html', asyn t.equal(typeof res.payload, 'string') }) +test('/documentation should display index html when noRedirect is true', async (t) => { + t.plan(4) + const fastify = Fastify() + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { noRedirect: true }) + + fastify.get('/', () => {}) + fastify.post('/', () => {}) + fastify.get('/example', schemaQuerystring, () => {}) + fastify.post('/example', schemaBody, () => {}) + fastify.get('/parameters/:id', schemaParams, () => {}) + fastify.get('/example1', schemaSecurity, () => {}) + + const res = await fastify.inject({ + method: 'GET', + url: '/documentation' + }) + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) + t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) +}) + test('/documentation/ should redirect to ./static/index.html', async (t) => { t.plan(3) const fastify = Fastify() @@ -144,6 +167,29 @@ test('/documentation/ should redirect to ./static/index.html', async (t) => { t.equal(typeof res.payload, 'string') }) +test('/documentation/ should display index html when noRedirect is true', async (t) => { + t.plan(4) + const fastify = Fastify() + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { noRedirect: true }) + + fastify.get('/', () => {}) + fastify.post('/', () => {}) + fastify.get('/example', schemaQuerystring, () => {}) + fastify.post('/example', schemaBody, () => {}) + fastify.get('/parameters/:id', schemaParams, () => {}) + fastify.get('/example1', schemaSecurity, () => {}) + + const res = await fastify.inject({ + method: 'GET', + url: '/documentation' + }) + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) + t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) +}) + test('/v1/documentation should redirect to ./documentation/static/index.html', async (t) => { t.plan(3) const fastify = Fastify() @@ -166,6 +212,29 @@ test('/v1/documentation should redirect to ./documentation/static/index.html', a t.equal(typeof res.payload, 'string') }) +test('/v1/documentation should display index html when noRedirect is true', async (t) => { + t.plan(4) + const fastify = Fastify() + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { routePrefix: '/v1/documentation', noRedirect: true }) + + fastify.get('/', () => {}) + fastify.post('/', () => {}) + fastify.get('/example', schemaQuerystring, () => {}) + fastify.post('/example', schemaBody, () => {}) + fastify.get('/parameters/:id', schemaParams, () => {}) + fastify.get('/example1', schemaSecurity, () => {}) + + const res = await fastify.inject({ + method: 'GET', + url: '/v1/documentation' + }) + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) + t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) +}) + test('/v1/documentation/ should redirect to ./static/index.html', async (t) => { t.plan(3) const fastify = Fastify() @@ -188,6 +257,29 @@ test('/v1/documentation/ should redirect to ./static/index.html', async (t) => { t.equal(typeof res.payload, 'string') }) +test('/v1/documentation/ should display index html when noRedirect is true', async (t) => { + t.plan(4) + const fastify = Fastify() + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { routePrefix: '/v1/documentation', noRedirect: true }) + + fastify.get('/', () => {}) + fastify.post('/', () => {}) + fastify.get('/example', schemaQuerystring, () => {}) + fastify.post('/example', schemaBody, () => {}) + fastify.get('/parameters/:id', schemaParams, () => {}) + fastify.get('/example1', schemaSecurity, () => {}) + + const res = await fastify.inject({ + method: 'GET', + url: '/v1/documentation/' + }) + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) + t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) +}) + test('/v1/foobar should redirect to ./foobar/static/index.html - in plugin', async (t) => { t.plan(3) const fastify = Fastify() @@ -213,6 +305,32 @@ test('/v1/foobar should redirect to ./foobar/static/index.html - in plugin', asy t.equal(typeof res.payload, 'string') }) +test('/v1/foobar should display index html when noRedirect is true', async (t) => { + t.plan(4) + const fastify = Fastify() + + fastify.register(async function (fastify, options) { + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { routePrefix: '/foobar', noRedirect: true }) + + fastify.get('/', () => {}) + fastify.post('/', () => {}) + fastify.get('/example', schemaQuerystring, () => {}) + fastify.post('/example', schemaBody, () => {}) + fastify.get('/parameters/:id', schemaParams, () => {}) + fastify.get('/example1', schemaSecurity, () => {}) + }, { prefix: '/v1' }) + + const res = await fastify.inject({ + method: 'GET', + url: '/v1/foobar' + }) + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) + t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) +}) + test('/v1/foobar/ should redirect to ./static/index.html - in plugin', async (t) => { t.plan(3) const fastify = Fastify() @@ -238,6 +356,32 @@ test('/v1/foobar/ should redirect to ./static/index.html - in plugin', async (t) t.equal(typeof res.payload, 'string') }) +test('/v1/foobar/ should display index html when noRedirect is true', async (t) => { + t.plan(4) + const fastify = Fastify() + + fastify.register(async function (fastify, options) { + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { routePrefix: '/foobar', noRedirect: true }) + + fastify.get('/', () => {}) + fastify.post('/', () => {}) + fastify.get('/example', schemaQuerystring, () => {}) + fastify.post('/example', schemaBody, () => {}) + fastify.get('/parameters/:id', schemaParams, () => {}) + fastify.get('/example1', schemaSecurity, () => {}) + }, { prefix: '/v1' }) + + const res = await fastify.inject({ + method: 'GET', + url: '/v1/foobar/' + }) + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) + t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) +}) + test('with routePrefix: \'/\' should redirect to ./static/index.html', async (t) => { t.plan(3) const fastify = Fastify() @@ -256,6 +400,25 @@ test('with routePrefix: \'/\' should redirect to ./static/index.html', async (t) t.equal(typeof res.payload, 'string') }) +test('with routePrefix: \'/\' should display index html when noRedirect is true', async (t) => { + t.plan(4) + const fastify = Fastify() + + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { routePrefix: '/', noRedirect: true }) + + fastify.get('/foo', () => {}) + + const res = await fastify.inject({ + method: 'GET', + url: '/' + }) + t.equal(res.statusCode, 200) + t.equal(res.headers.location, undefined) + t.equal(typeof res.payload, 'string') + t.equal('text/html; charset=utf-8', res.headers['content-type']) +}) + test('/documentation/static/:file should send back the correct file', async (t) => { t.plan(21) const fastify = Fastify() @@ -543,3 +706,37 @@ test('should return empty log level of route /documentation', async (t) => { t.equal(res.statusCode, 302) t.equal(res.headers.location, './static/index.html') }) + +test('/documentation/static/index.html should display index html with correct asset urls', async (t) => { + t.plan(4) + const fastify = Fastify() + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { theme: { js: [{ filename: 'theme-js.js' }] } }) + + const res = await fastify.inject({ + method: 'GET', + url: '/documentation/static/index.html' + }) + + t.equal(res.payload.includes('href="./static/index.css"'), false) + t.equal(res.payload.includes('src="./static/theme/theme-js.js"'), false) + t.equal(res.payload.includes('href="./index.css"'), true) + t.equal(res.payload.includes('src="./theme/theme-js.js"'), true) +}) + +test('/documentation should display index html with correct asset urls when noRedirect is true', async (t) => { + t.plan(4) + const fastify = Fastify() + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { noRedirect: true, theme: { js: [{ filename: 'theme-js.js' }] } }) + + const res = await fastify.inject({ + method: 'GET', + url: '/documentation' + }) + + t.equal(res.payload.includes('href="./static/index.css"'), true) + t.equal(res.payload.includes('src="./static/theme/theme-js.js"'), true) + t.equal(res.payload.includes('href="./index.css"'), false) + t.equal(res.payload.includes('src="./theme/theme-js.js"'), false) +})