Repository: nestjs/swagger (ts)

Issue:
Blank page with script error
## Bug
<!-- 
Please search GitHub for a similar issue or PR before submitting.
Check one of the following options with "x" -->
<pre><code>
[ ] Regression <!--(a behavior that used to work and stopped working in a new release)-->
[x] Bug report
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.
</code></pre>

## Current behavior
After updating to the latest version of the package and nest my page started to show blank and a script error 


> Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-iV83EgAQc1+Q++O7L1ZemfWFbYYPNv2syB2HreE5S/8='), or a nonce ('nonce-...') is required to enable inline execution.



## Expected behavior
Should display the docs page 

## Environment

<pre><code>
Nest version: X.Y.Z
 "@nestjs/swagger": "^4.6.0",
"@nestjs/core": "^7.4.2",
"fastify-swagger": "^3.3.0",
 "@nestjs/platform-fastify": "^7.4.2",
 
For Tooling issues:
- Node version: 14
- Platform: Mac

Others:
<!-- Anything else relevant?  Operating system version, IDE, package manager, ... -->
</code></pre>


Pull request:
feat(): add fastify-swagger options
Allow the use of "initOAuth", "staticCSP" and "transformStaticCSP" options to fastify-swagger

Closes #932

## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
```
[ ] Bugfix
[x] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Other... Please describe:
```

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Using @nestjs/swagger with Fastify, you cannot pass the "initOAuth", "staticCSP" and "transformStaticCSP" options at fastify-swagger plugin registration.

Issue Number: #932


## What is the new behavior?

You can pass on "initOAuth", "staticCSP" and "transformStaticCSP" as per [fastify-swagger's documentation](https://github.com/fastify/fastify-swagger#options).

## Does this PR introduce a breaking change?
```
[ ] Yes
[x] No
```

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->


## Other information

The existing solution to the problem #932 does not satisfy Content Security Policy (CSP) ["unsafe-inline keyword"](https://content-security-policy.com/unsafe-inline/)
I found the staticCSP option in the fastify-swagger module. If this option is set to "true", then [the hash is used](https://content-security-policy.com/hash/
)
Analysis of the other available parameters showed that the associated transformStaticCSP parameter could also be transfer. And also the initOAuth parameter, which may be useful to someone.

Patch:
diff --git a/lib/interfaces/swagger-custom-options.interface.ts b/lib/interfaces/swagger-custom-options.interface.ts
index 66068f88..4a729f2c 100644
--- a/lib/interfaces/swagger-custom-options.interface.ts
+++ b/lib/interfaces/swagger-custom-options.interface.ts
@@ -14,6 +14,9 @@ export interface ExpressSwaggerCustomOptions {
 
 export interface FastifySwaggerCustomOptions {
   uiConfig?: Record<string, any>;
+  initOAuth?: Record<string, any>;
+  staticCSP?: boolean | string | Record<string, string | string[]>;
+  transformStaticCSP?: (header: string) => string;
 }
 
 export type SwaggerCustomOptions =
diff --git a/lib/swagger-module.ts b/lib/swagger-module.ts
index 547d25a2..0b4c8d91 100644
--- a/lib/swagger-module.ts
+++ b/lib/swagger-module.ts
@@ -103,7 +103,10 @@ export class SwaggerModule {
           specification: {
             document
           },
-          uiConfig: (options || {}).uiConfig
+          uiConfig: options?.uiConfig,
+          initOAuth: options?.initOAuth,
+          staticCSP: options?.staticCSP,
+          transformStaticCSP: options?.transformStaticCSP
         }
       );
     });


Test patch:
diff --git a/e2e/fastify.e2e-spec.ts b/e2e/fastify.e2e-spec.ts
index d20b1a3f..38146746 100644
--- a/e2e/fastify.e2e-spec.ts
+++ b/e2e/fastify.e2e-spec.ts
@@ -87,4 +87,105 @@ describe('Fastify Swagger', () => {
       app.getHttpAdapter().getInstance().ready()
     ).resolves.toBeDefined();
   });
+
+  it('should pass initOAuth options to fastify-swagger', async () => {
+    const document1 = SwaggerModule.createDocument(app, builder.build());
+    const initOAuth = {
+      scopes: ['openid', 'profile', 'email', 'offline_access']
+    };
+    const options = { initOAuth };
+    SwaggerModule.setup('/swagger1', app, document1, options);
+
+    const instance = await app.getHttpAdapter().getInstance().ready();
+
+    instance.ready(async () => {
+      const response = await instance.inject({
+        method: 'GET',
+        url: '/swagger1/initOAuth'
+      });
+
+      expect(response.statusCode).toEqual(200);
+      expect(JSON.parse(response.body)).toEqual(initOAuth);
+    });
+  });
+
+  it('should pass staticCSP = undefined options to fastify-swagger', async () => {
+    const document1 = SwaggerModule.createDocument(app, builder.build());
+    SwaggerModule.setup('swagger1', app, document1);
+
+    const instance = await app.getHttpAdapter().getInstance().ready();
+
+    instance.ready(async () => {
+      const response = await instance.inject({
+        method: 'GET',
+        url: '/swagger1/json'
+      });
+
+      expect(response.statusCode).toEqual(200);
+      expect(response.headers['content-security-policy']).toBeUndefined();
+    });
+  });
+
+  it('should pass staticCSP = true options to fastify-swagger', async () => {
+    const document1 = SwaggerModule.createDocument(app, builder.build());
+    const options = { staticCSP: true };
+    SwaggerModule.setup('/swagger1', app, document1, options);
+
+    const instance = await app.getHttpAdapter().getInstance().ready();
+
+    instance.ready(async () => {
+      const response = await instance.inject({
+        method: 'GET',
+        url: '/swagger1/json'
+      });
+
+      expect(response.statusCode).toEqual(200);
+      expect(response.headers['content-security-policy']).toContain(`script-src 'self' 'sha256`);
+      expect(response.headers['content-security-policy']).toContain(`style-src 'self' https: 'sha256`);
+    });
+  });
+
+  it('should pass staticCSP = false options to fastify-swagger', async () => {
+    const document1 = SwaggerModule.createDocument(app, builder.build());
+    const options = { staticCSP: false };
+    SwaggerModule.setup('/swagger1', app, document1, options);
+
+    const instance = await app.getHttpAdapter().getInstance().ready();
+
+    instance.ready(async () => {
+      const response = await instance.inject({
+        method: 'GET',
+        url: '/swagger1/json'
+      });
+
+      expect(response.statusCode).toEqual(200);
+      expect(response.headers['content-security-policy']).toBeUndefined();
+    });
+  });
+
+  it('should pass transformStaticCSP = function options to fastify-swagger', async () => {
+    const document1 = SwaggerModule.createDocument(app, builder.build());
+    const checkParam = jest.fn((param: string) => param);
+    const options = {
+      staticCSP: `default-src 'self';`,
+      transformStaticCSP: (header: string) => {
+        checkParam(header);
+        return `default-src 'self'; script-src 'self';`
+      }
+    };
+    SwaggerModule.setup('/swagger1', app, document1, options);
+
+    const instance = await app.getHttpAdapter().getInstance().ready();
+
+    instance.ready(async () => {
+      const response = await instance.inject({
+        method: 'GET',
+        url: '/swagger1/json'
+      });
+
+      expect(checkParam).toBeCalledWith(`default-src 'self';`);
+      expect(response.statusCode).toEqual(200);
+      expect(response.headers['content-security-policy']).toEqual(`default-src 'self'; script-src 'self';`);
+    });
+  });
 });