Skip to content

Commit

Permalink
Add option for noRedirect to serve UI directly at the route prefix
Browse files Browse the repository at this point in the history
  • Loading branch information
jdhollander committed May 2, 2024
1 parent 7d1fffc commit 09010c2
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 33 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ async function fastifySwaggerUi (fastify, opts) {
hooks: opts.uiHooks,
theme: opts.theme || {},
logo: logoContent,
noRedirect: opts.noRedirect,
...opts
})
}
Expand Down
21 changes: 11 additions & 10 deletions lib/index-html.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
'use strict'

function indexHtml (opts) {
const staticPrefix = opts.noRedirect ? opts.staticPrefix : ''
return `<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${opts.theme?.title || 'Swagger UI'}</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="./index.css" />
${opts.theme && opts.theme.css ? opts.theme.css.map(css => `<link rel="stylesheet" type="text/css" href="./theme/${css.filename}" />\n`).join('') : ''}
<link rel="stylesheet" type="text/css" href=".${staticPrefix}/swagger-ui.css" />
<link rel="stylesheet" type="text/css" href=".${staticPrefix}/index.css" />
${opts.theme && opts.theme.css ? opts.theme.css.map(css => `<link rel="stylesheet" type="text/css" href=".${staticPrefix}/theme/${css.filename}" />\n`).join('') : ''}
${opts.theme && opts.theme.favicon
? opts.theme.favicon.map(favicon => `<link rel="${favicon.rel}" type="${favicon.type}" href="./theme/${favicon.filename}" sizes="${favicon.sizes}" />\n`).join('')
? opts.theme.favicon.map(favicon => `<link rel="${favicon.rel}" type="${favicon.type}" href=".${staticPrefix}/theme/${favicon.filename}" sizes="${favicon.sizes}" />\n`).join('')
: `
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<link rel="icon" type="image/png" href=".${staticPrefix}/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href=".${staticPrefix}/favicon-16x16.png" sizes="16x16" />
`}
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src="./swagger-initializer.js" charset="UTF-8"> </script>
${opts.theme && opts.theme.js ? opts.theme.js.map(js => `<script src="./theme/${js.filename}" charset="UTF-8"> </script>\n`).join('') : ''}
<script src=".${staticPrefix}/swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src=".${staticPrefix}/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src=".${staticPrefix}/swagger-initializer.js" charset="UTF-8"> </script>
${opts.theme && opts.theme.js ? opts.theme.js.map(js => `<script src=".${staticPrefix}/theme/${js.filename}" charset="UTF-8"> </script>\n`).join('') : ''}
</body>
</html>
`
Expand Down
60 changes: 37 additions & 23 deletions lib/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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({
Expand Down
197 changes: 197 additions & 0 deletions test/route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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)
})

0 comments on commit 09010c2

Please sign in to comment.