Skip to content

Commit

Permalink
fix(theme): allow a csp nonce to be specified (#15359)
Browse files Browse the repository at this point in the history
resolves #15358
  • Loading branch information
hendric-dev committed Jul 5, 2022
1 parent 9abf709 commit 0a3d070
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 5 deletions.
24 changes: 24 additions & 0 deletions packages/docs/src/pages/en/features/theme.md
Expand Up @@ -248,6 +248,30 @@ interface ThemeInstance {
}
```

## CSP Nonce

Pages with the `script-src` or `style-src` CSP rules enabled may require a **nonce** to be specified for embedded style tags.

```html
<!-- Use with script-src -->
Content-Security-Policy: script-src 'self' 'nonce-dQw4w9WgXcQ'

<!-- Use with style-src -->
Content-Security-Policy: style-src 'self' 'nonce-dQw4w9WgXcQ'
```

```ts
// src/plugins/vuetify.js

import {createVuetify} from 'vuetify'

export const vuetify = createVuetify({
theme: {
cspNonce: 'dQw4w9WgXcQ'
}
})
```

## Implementation

Vuetify generates theme styles at runtime according to the given configuration. The generated styles are injected into the `<head>` section of the DOM in a `<style>` tag with an **id** of `vuetify-theme-stylesheet`.
Expand Down
14 changes: 14 additions & 0 deletions packages/vuetify/src/composables/__tests__/theme.spec.ts
Expand Up @@ -81,6 +81,20 @@ describe('createTheme', () => {
expect(theme.computedThemes.value.light.colors.background).toBe('#FF0000')
})

it('should set a CSP nonce if configured', async () => {
createTheme(app, { cspNonce: 'my-csp-nonce' })

const styleElement = document.getElementById('vuetify-theme-stylesheet')
expect(styleElement?.getAttribute('nonce')).toBe('my-csp-nonce')
})

it('should not set a CSP nonce if option was left blank', async () => {
createTheme(app, {})

const styleElement = document.getElementById('vuetify-theme-stylesheet')
expect(styleElement?.getAttribute('nonce')).toBeNull()
})

// it('should use vue-meta@2.3 functionality', () => {
// const theme = createTheme()
// const set = jest.fn()
Expand Down
16 changes: 11 additions & 5 deletions packages/vuetify/src/composables/theme.ts
Expand Up @@ -25,18 +25,20 @@ import { APCAcontrast } from '@/util/color/APCA'

// Types
import type { App, DeepReadonly, InjectionKey, Ref } from 'vue'
import type { HeadClient } from '@vueuse/head'
import type { HeadAttrs, HeadClient } from '@vueuse/head'

type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T

export type ThemeOptions = false | {
cspNonce?: string
defaultTheme?: string
variations?: false | VariationsOptions
themes?: Record<string, ThemeDefinition>
}
export type ThemeDefinition = DeepPartial<InternalThemeDefinition>

interface InternalThemeOptions {
cspNonce?: string
isDisabled: boolean
defaultTheme: string
variations: false | VariationsOptions
Expand Down Expand Up @@ -294,13 +296,16 @@ export function createTheme (app: App, options?: ThemeOptions): ThemeInstance {
})

if (head) {
head.addHeadObjs(computed(() => ({
style: [{
head.addHeadObjs(computed(() => {
const style: HeadAttrs = {
children: styles.value,
type: 'text/css',
id: 'vuetify-theme-stylesheet',
}],
})))
}
if (parsedOptions.cspNonce) style.nonce = parsedOptions.cspNonce

return { style: [style] }
}))

if (IN_BROWSER) {
watchEffect(() => head.updateDOM())
Expand All @@ -318,6 +323,7 @@ export function createTheme (app: App, options?: ThemeOptions): ThemeInstance {
const el = document.createElement('style')
el.type = 'text/css'
el.id = 'vuetify-theme-stylesheet'
if (parsedOptions.cspNonce) el.setAttribute('nonce', parsedOptions.cspNonce)

styleEl = el
document.head.appendChild(styleEl)
Expand Down

0 comments on commit 0a3d070

Please sign in to comment.