@dargmuesli/nuxt-vio 11.2.6 → 12.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app.config.ts CHANGED
@@ -1,80 +1,63 @@
1
1
  export default defineAppConfig({
2
2
  vio: {
3
3
  pages: undefined,
4
- server: {
5
- middleware: {
6
- headers: {
7
- csp: {
8
- default: {
9
- NEL: '\'{"report_to":"default","max_age":31536000,"include_subdomains":true}\'',
10
- 'Report-To':
11
- '\'{"group":"default":"max_age":31536000:"endpoints":[{"url":"https://dargmuesli.report-uri.com/a/d/g"}]:"include_subdomains":true}\'',
12
- },
13
- production: {} as Record<string, string>,
14
- },
15
- },
16
- },
17
- },
18
4
  themeColor: undefined,
19
5
  },
20
6
  })
21
7
 
22
- declare module 'nuxt/schema' {
23
- interface AppConfigInput {
24
- vio: {
25
- pages?: {
26
- legalNotice?: {
27
- contact: {
28
- email: string
8
+ export type AppConfig = {
9
+ vio: {
10
+ pages?: {
11
+ legalNotice?: {
12
+ contact: {
13
+ email: string
14
+ }
15
+ responsibility: {
16
+ address: {
17
+ city: string
18
+ name: string
19
+ street: string
20
+ }
21
+ }
22
+ tmg: {
23
+ address: {
24
+ city: string
25
+ name: string
26
+ street: string
29
27
  }
30
- responsibility: {
28
+ }
29
+ }
30
+ privacyPolicy?: {
31
+ hostingCdn?: {
32
+ external: {
31
33
  address: {
32
34
  city: string
33
35
  name: string
34
36
  street: string
35
37
  }
36
38
  }
37
- tmg: {
39
+ }
40
+ mandatoryInfo?: {
41
+ responsible: {
38
42
  address: {
39
43
  city: string
44
+ email: string
40
45
  name: string
41
46
  street: string
42
47
  }
43
48
  }
44
49
  }
45
- privacyPolicy?: {
46
- hostingCdn?: {
47
- external: {
48
- address: {
49
- city: string
50
- name: string
51
- street: string
52
- }
53
- }
54
- }
55
- mandatoryInfo?: {
56
- responsible: {
57
- address: {
58
- city: string
59
- email: string
60
- name: string
61
- street: string
62
- }
63
- }
64
- }
65
- }
66
50
  }
67
- server?: {
68
- middleware: {
69
- headers: {
70
- csp: {
71
- default: Record<string, string>
72
- production: Record<string, string>
73
- }
74
- }
75
- }
51
+ }
52
+ server?: {
53
+ middleware: {
54
+ headers: Record<string, string>
76
55
  }
77
- themeColor?: string
78
56
  }
57
+ themeColor?: string
79
58
  }
80
59
  }
60
+
61
+ declare module 'nuxt/schema' {
62
+ interface AppConfigInput extends AppConfig {}
63
+ }
@@ -0,0 +1,11 @@
1
+ import type { Dayjs } from 'dayjs'
2
+
3
+ export const useDateTime = () => {
4
+ const { $dayjs, ssrContext } = useNuxtApp()
5
+ const timezone = ssrContext
6
+ ? ssrContext.event.context.$timezone
7
+ : getTimezone()
8
+
9
+ return (dateTime?: string | number | Dayjs | Date | null) =>
10
+ $dayjs(dateTime).tz(timezone)
11
+ }
@@ -10,8 +10,8 @@ export const useAppLayout = () => {
10
10
  },
11
11
  })
12
12
 
13
- // TODO: convert to `useServerHeadSafe` (https://github.com/harlan-zw/nuxt-seo-kit/issues/98)
14
- useSeoMeta({
13
+ // TODO: convert to `useServerHeadSafe` (https://github.com/unjs/unhead/issues/221)
14
+ useServerSeoMeta({
15
15
  titleTemplate: (title) =>
16
16
  TITLE_TEMPLATE({
17
17
  siteName: siteConfig.name,
package/nuxt.config.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  TIMEZONE_COOKIE_NAME,
10
10
  GTAG_COOKIE_ID,
11
11
  VIO_NUXT_BASE_CONFIG,
12
+ GET_CSP,
12
13
  } from './utils/constants'
13
14
 
14
15
  const currentDir = dirname(fileURLToPath(import.meta.url))
@@ -29,9 +30,6 @@ export default defineNuxtConfig(
29
30
  },
30
31
  },
31
32
  devtools: {
32
- enabled:
33
- process.env.NODE_ENV === 'development' &&
34
- !process.env.NUXT_PUBLIC_VIO_IS_TESTING,
35
33
  timeline: {
36
34
  enabled: true,
37
35
  },
@@ -48,31 +46,42 @@ export default defineNuxtConfig(
48
46
  '@nuxtjs/tailwindcss',
49
47
  '@pinia/nuxt',
50
48
  'nuxt-gtag',
49
+ (_options, nuxt) => {
50
+ if (nuxt.options._generate) {
51
+ nuxt.options.features.inlineStyles = false
52
+ }
53
+ },
51
54
  // nuxt-security: remove invalid `'none'`s and duplicates
52
55
  (_options, nuxt) => {
53
- const nuxtConfigSecurity = nuxt.options.security
56
+ const nuxtConfigSecurityHeaders = nuxt.options.security.headers
54
57
 
55
58
  if (
56
- typeof nuxtConfigSecurity.headers !== 'boolean' &&
57
- nuxtConfigSecurity.headers.contentSecurityPolicy &&
58
- typeof nuxtConfigSecurity.headers.contentSecurityPolicy !==
59
- 'boolean' &&
60
- typeof nuxtConfigSecurity.headers.contentSecurityPolicy !== 'string'
59
+ typeof nuxtConfigSecurityHeaders !== 'boolean' &&
60
+ nuxtConfigSecurityHeaders.contentSecurityPolicy
61
61
  ) {
62
- for (const [key, value] of Object.entries(
63
- nuxtConfigSecurity.headers.contentSecurityPolicy,
64
- )) {
62
+ if (nuxt.options._generate) {
63
+ nuxtConfigSecurityHeaders.contentSecurityPolicy = defu(
64
+ {
65
+ 'script-src-elem': [
66
+ "'unsafe-inline'", // nuxt-color-mode (https://github.com/nuxt-modules/color-mode/issues/266), runtimeConfig (static)
67
+ ],
68
+ },
69
+ GET_CSP(SITE_URL),
70
+ nuxtConfigSecurityHeaders.contentSecurityPolicy,
71
+ )
72
+ }
73
+
74
+ const csp = nuxtConfigSecurityHeaders.contentSecurityPolicy
75
+
76
+ for (const [key, value] of Object.entries(csp)) {
65
77
  if (!Array.isArray(value)) continue
66
78
 
67
79
  const valueFiltered = value.filter((x) => x !== "'none'")
68
80
 
69
81
  if (valueFiltered.length) {
70
- ;(
71
- nuxtConfigSecurity.headers.contentSecurityPolicy as Record<
72
- string,
73
- unknown
74
- >
75
- )[key] = [...new Set(valueFiltered)]
82
+ ;(csp as Record<string, unknown>)[key] = [
83
+ ...new Set(valueFiltered),
84
+ ]
76
85
  }
77
86
  }
78
87
  }
@@ -81,32 +90,24 @@ export default defineNuxtConfig(
81
90
  ],
82
91
  nitro: {
83
92
  compressPublicAssets: true,
84
- experimental: {
85
- openAPI: process.env.NODE_ENV === 'development',
86
- },
87
93
  },
88
94
  runtimeConfig: {
89
95
  public: {
90
- i18n: {
91
- ...(process.env.NODE_ENV === 'development'
92
- ? {}
93
- : { baseUrl: SITE_URL }),
96
+ site: {
97
+ url: SITE_URL,
94
98
  },
95
99
  vio: {
96
- isInProduction: process.env.NODE_ENV === 'production',
97
100
  isTesting: false,
98
101
  },
99
102
  },
100
103
  },
101
- typescript: {
102
- shim: false,
103
- strict: true,
104
- // tsConfig: {
105
- // compilerOptions: {
106
- // noErrorTruncation: true,
107
- // },
108
- // },
109
- },
104
+ // typescript: {
105
+ // tsConfig: {
106
+ // compilerOptions: {
107
+ // noErrorTruncation: true,
108
+ // },
109
+ // },
110
+ // },
110
111
 
111
112
  // modules
112
113
  colorMode: {
@@ -160,9 +161,7 @@ export default defineNuxtConfig(
160
161
  },
161
162
  gtag: {
162
163
  config: {
163
- cookie_flags: `samesite=strict${
164
- process.env.NODE_ENV === 'production' ? ';secure' : ''
165
- }`,
164
+ cookie_flags: 'samesite=strict',
166
165
  },
167
166
  enabled: false,
168
167
  initCommands: [
@@ -195,164 +194,43 @@ export default defineNuxtConfig(
195
194
  },
196
195
  security: {
197
196
  headers: {
198
- contentSecurityPolicy: defu(
199
- {
200
- // Cloudflare
201
- ...(process.env.NODE_ENV === 'production'
202
- ? {
203
- 'connect-src': ['https://cloudflareinsights.com'],
204
- 'script-src': ['https://static.cloudflareinsights.com'], // TODO: replace with `script-src-elem` once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_script-src-elem)
205
- }
206
- : {}),
207
- },
208
- {
209
- // Google Analytics 4 (https://developers.google.com/tag-platform/tag-manager/web/csp)
210
- 'connect-src': [
211
- 'https://*.analytics.google.com',
212
- 'https://*.google-analytics.com',
213
- 'https://*.googletagmanager.com',
214
- ],
215
- 'img-src': [
216
- 'https://*.google-analytics.com',
217
- 'https://*.googletagmanager.com',
218
- ],
219
- 'script-src': ['https://*.googletagmanager.com'], // TODO: replace with `script-src-elem` once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_script-src-elem)
220
- },
221
- {
222
- // vio
223
- 'connect-src': ["'self'"], // `${SITE_URL}/api/healthcheck`
224
- 'manifest-src': [`${SITE_URL}/site.webmanifest`],
225
- 'script-src': [
226
- 'https://polyfill.io/v3/polyfill.min.js', // ESLint plugin compat
227
- ], // TODO: replace with `script-src-elem` once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_script-src-elem)
228
- },
229
- {
230
- // @nuxt/devtools
231
- ...(process.env.NODE_ENV === 'development'
232
- ? {
233
- 'frame-src': [
234
- 'http://localhost:3000/__nuxt_devtools__/client/',
235
- ],
236
- }
237
- : {}),
238
- },
239
- {
240
- // nuxt-i18n
241
- ...(process.env.NODE_ENV === 'development'
242
- ? {}
243
- : {
244
- 'script-src': ["'self'"], // 'http://localhost:3000/_nuxt/i18n.config.*.js' // TOD: add with subresource integrity?
245
- }),
246
- },
247
- {
248
- // nuxt-link-checker
249
- ...(process.env.NODE_ENV === 'development'
250
- ? {
251
- 'connect-src': ["'self'"], // 'http://localhost:3000/api/__link_checker__/inspect'
252
- }
253
- : {}),
254
- },
255
- {
256
- // nuxt-og-image
257
- ...(process.env.NODE_ENV === 'development'
258
- ? {
259
- 'font-src': ['https://fonts.gstatic.com/s/inter/'],
260
- 'frame-ancestors': ["'self'"],
261
- 'frame-src': ["'self'"],
262
- 'script-src': ['https://cdn.tailwindcss.com/'], // TODO: replace with `script-src-elem` once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_script-src-elem)
263
- 'style-src': [
264
- // TODO: replace with `style-src-elem` once Webkit supports it
265
- 'https://cdn.jsdelivr.net/npm/gardevoir https://fonts.googleapis.com/css2',
266
- ],
267
- }
268
- : {}),
269
- },
270
- {
271
- // nuxt-simple-sitemap
272
- 'script-src': [`${SITE_URL}/__sitemap__/style.xsl`], // TODO: replace with `script-src-elem` once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_script-src-elem)
273
- },
274
- {
275
- // nuxt
276
- 'connect-src': [
277
- ...(process.env.NODE_ENV === 'development'
278
- ? [
279
- 'http://localhost:3000/_nuxt/', // hot reload
280
- 'https://localhost:3000/_nuxt/', // hot reload
281
- 'ws://localhost:3000/_nuxt/', // hot reload
282
- 'wss://localhost:3000/_nuxt/', // hot reload
283
- ]
284
- : ["'self'"]), // build metadata and payloads
285
- ],
286
- 'img-src': [
287
- "'self'", // TODO: replace with `"'nonce-{{nonce}}'",`
288
- 'data:', // external link icon
289
- ],
290
- 'script-src': ["'nonce-{{nonce}}'"], // TODO: replace with `script-src-elem` once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_script-src-elem)
291
- 'style-src': [
292
- // TODO: replace with `style-src-elem` once Webkit supports it
293
- "'self'", // TODO: replace with `"'nonce-{{nonce}}'",` (https://github.com/vitejs/vite/pull/11864)
294
- "'unsafe-inline'", // TODO: replace with `"'nonce-{{nonce}}'",` (https://github.com/vitejs/vite/pull/11864)
295
- ],
296
- },
297
- {
298
- // nitro
299
- 'connect-src': ["'self'"] /* swagger
300
- 'http://localhost:3000/_nitro/openapi.json',
301
- 'http://localhost:3000/_nitro/swagger', */,
302
- 'script-src': [
303
- 'https://cdn.jsdelivr.net/npm/', // swagger // TODO: increase precision (https://github.com/unjs/nitro/issues/1757)
304
- ], // TODO: replace with `script-src-elem` once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_script-src-elem)
305
- 'style-src': [
306
- 'https://cdn.jsdelivr.net/npm/', // swagger // TODO: increase precision (https://github.com/unjs/nitro/issues/1757)
307
- ],
308
- },
309
- {
310
- // base
311
- 'base-uri': ["'none'"], // does not fallback to `default-src`
312
- 'child-src': false as const,
313
- 'connect-src': false as const,
314
- 'default-src': ["'none'"],
315
- 'font-src': false as const,
316
- 'form-action': ["'none'"], // does not fallback to `default-src`
317
- 'frame-ancestors': ["'none'"], // does not fallback to `default-src`
318
- 'frame-src': false as const,
319
- 'img-src': false as const,
320
- 'media-src': false as const,
321
- 'navigate-to': false as const,
322
- 'object-src': false as const,
323
- 'prefetch-src': false as const,
324
- 'report-to': undefined,
325
- 'report-uri': false as const,
326
- // TODO: evaluate header (https://github.com/maevsi/maevsi/issues/830) // https://stackoverflow.com/questions/62081028/this-document-requires-trustedscripturl-assignment
327
- // 'require-trusted-types-for': ["'script'"], // csp-evaluator
328
- sandbox: false as const,
329
- 'script-src': false as const,
330
- 'script-src-attr': false as const, // TODO: enable once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_script-src-attr)
331
- 'script-src-elem': false as const, // TODO: enable once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_script-src-elem)
332
- 'style-src': false as const,
333
- 'style-src-attr': false as const, // TODO: enable once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_style-src-attr)
334
- 'style-src-elem': false as const, // TODO: enable once Webkit supports it (https://caniuse.com/mdn-http_headers_content-security-policy_style-src-elem)
335
- 'upgrade-insecure-requests': false, // TODO: set to `process.env.NODE_ENV === 'production'` or `true` when tests run on https
336
- 'worker-src': false as const,
337
- },
338
- ),
339
- crossOriginEmbedderPolicy: false, // https://stackoverflow.com/questions/71904052/getting-notsameoriginafterdefaultedtosameoriginbycoep-error-with-helmet
340
- strictTransportSecurity:
341
- process.env.NODE_ENV === 'production'
342
- ? {
343
- maxAge: 31536000,
344
- includeSubdomains: true,
345
- preload: true,
346
- }
347
- : false,
197
+ contentSecurityPolicy: {
198
+ 'base-uri': ["'none'"], // does not fallback to `default-src`
199
+ 'child-src': false as const,
200
+ 'connect-src': false as const,
201
+ 'default-src': ["'none'"],
202
+ 'font-src': false as const,
203
+ 'form-action': ["'none'"], // does not fallback to `default-src`
204
+ 'frame-ancestors': ["'none'"], // does not fallback to `default-src`
205
+ 'frame-src': false as const,
206
+ 'img-src': false as const,
207
+ 'media-src': false as const,
208
+ 'navigate-to': false as const,
209
+ 'object-src': false as const,
210
+ 'prefetch-src': false as const,
211
+ 'report-to': undefined,
212
+ 'report-uri': false as const,
213
+ // 'require-trusted-types-for': ["'script'"], // csp-evaluator // TODO: wait for trusted type support in vue (https://github.com/vuejs/core/pull/10844)
214
+ sandbox: false as const,
215
+ 'script-src': false as const,
216
+ 'script-src-attr': false as const,
217
+ 'script-src-elem': false as const,
218
+ 'style-src': false as const,
219
+ 'style-src-attr': false as const,
220
+ 'style-src-elem': false as const,
221
+ 'upgrade-insecure-requests': false, // TODO: set to `process.env.NODE_ENV === 'production'` or `true` when tests run on https
222
+ 'worker-src': false as const,
223
+ },
348
224
  xXSSProtection: '1; mode=block', // TODO: set back to `0` once CSP does not use `unsafe-*` anymore (https://github.com/maevsi/maevsi/issues/1047)
349
225
  },
226
+ ssg: {
227
+ hashStyles: true,
228
+ },
350
229
  },
351
230
  seo: {
352
231
  splash: false,
353
232
  },
354
233
  site: {
355
- debug: process.env.NODE_ENV === 'development',
356
234
  id: 'vio',
357
235
  url: SITE_URL,
358
236
  },
@@ -365,9 +243,58 @@ export default defineNuxtConfig(
365
243
 
366
244
  // environments
367
245
  $development: {
246
+ devtools: {
247
+ enabled: !process.env.NUXT_PUBLIC_VIO_IS_TESTING,
248
+ },
249
+ nitro: {
250
+ experimental: {
251
+ openAPI: true,
252
+ },
253
+ },
254
+ runtimeConfig: {
255
+ public: {
256
+ vio: {
257
+ isInProduction: false,
258
+ },
259
+ },
260
+ },
261
+
368
262
  // modules
369
263
  security: {
370
- rateLimiter: false, // TODO: enable when nuxt-link-checker bundles requests (https://github.com/harlan-zw/nuxt-link-checker/issues/21)
264
+ headers: {
265
+ crossOriginEmbedderPolicy: 'unsafe-none',
266
+ strictTransportSecurity: false, // prevent endless reload in Chrome
267
+ },
268
+ },
269
+ site: {
270
+ debug: true,
271
+ },
272
+ },
273
+ $production: {
274
+ runtimeConfig: {
275
+ public: {
276
+ i18n: {
277
+ baseUrl: SITE_URL,
278
+ },
279
+ vio: {
280
+ isInProduction: true,
281
+ },
282
+ },
283
+ },
284
+
285
+ // modules
286
+ gtag: {
287
+ config: {
288
+ cookie_flags: 'samesite=strict;secure',
289
+ },
290
+ },
291
+ security: {
292
+ headers: {
293
+ strictTransportSecurity: {
294
+ maxAge: 31536000,
295
+ preload: true,
296
+ },
297
+ },
371
298
  },
372
299
  },
373
300
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dargmuesli/nuxt-vio",
3
- "version": "11.2.6",
3
+ "version": "12.0.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/dargmuesli/vio.git"
@@ -87,6 +87,7 @@
87
87
  "prettier": "3.2.5",
88
88
  "prettier-plugin-tailwindcss": "0.5.14",
89
89
  "serve": "14.2.3",
90
+ "sharp": "0.33.4",
90
91
  "stylelint": "16.5.0",
91
92
  "stylelint-config-recommended-vue": "1.5.0",
92
93
  "stylelint-config-standard": "36.0.0",
@@ -95,8 +96,7 @@
95
96
  "ufo": "1.5.3",
96
97
  "unhead": "1.9.10",
97
98
  "vue": "3.4.27",
98
- "vue-router": "4.3.2",
99
- "vue-tsc": "2.0.17"
99
+ "vue-router": "4.3.2"
100
100
  },
101
101
  "peerDependencies": {
102
102
  "nuxt": "3.11.2",
@@ -1,29 +1,11 @@
1
- import type { H3Event } from 'h3'
2
- import type { AppConfig } from 'nuxt/schema'
1
+ import type { AppConfig } from '../../app.config'
3
2
 
4
3
  export default defineEventHandler(async (event) => {
5
- setRequestHeader(event, TIMEZONE_HEADER_KEY, await getTimezone(event))
6
- setResponseHeaders(event)
7
- })
8
-
9
- const setRequestHeader = (event: H3Event, name: string, value?: string) => {
10
- event.node.req.headers[name] = value
11
- }
12
-
13
- const setResponseHeaders = (event: H3Event) => {
14
4
  const config = useAppConfig() as AppConfig
15
5
 
16
- for (const entry of Object.entries(
17
- config.vio.server.middleware.headers.csp.default,
18
- )) {
19
- appendHeader(event, entry[0], entry[1])
20
- }
6
+ if (!config.vio.server) return
21
7
 
22
- if (process.env.NODE_ENV === 'production') {
23
- for (const entry of Object.entries(
24
- config.vio.server.middleware.headers.csp.production,
25
- )) {
26
- appendHeader(event, entry[0], entry[1])
27
- }
8
+ for (const entry of Object.entries(config.vio.server.middleware.headers)) {
9
+ appendHeader(event, entry[0], entry[1])
28
10
  }
29
- }
11
+ })
@@ -0,0 +1,9 @@
1
+ export default defineEventHandler(async (event) => {
2
+ event.context.$timezone = await getTimezoneServer(event)
3
+ })
4
+
5
+ declare module 'h3' {
6
+ interface H3EventContext {
7
+ $timezone?: string
8
+ }
9
+ }
@@ -0,0 +1,55 @@
1
+ import { defu } from 'defu'
2
+ import type { NuxtOptions } from 'nuxt/schema'
3
+ import { GET_CSP } from '../../utils/constants'
4
+
5
+ // remove invalid `'none'`s and duplicates
6
+ const cleanupCsp = (
7
+ nuxtSecurityConfiguration: Partial<NuxtOptions['security']>,
8
+ ) => {
9
+ if (
10
+ nuxtSecurityConfiguration.headers &&
11
+ typeof nuxtSecurityConfiguration.headers !== 'boolean' &&
12
+ nuxtSecurityConfiguration.headers.contentSecurityPolicy
13
+ ) {
14
+ const csp = nuxtSecurityConfiguration.headers.contentSecurityPolicy
15
+
16
+ for (const [key, value] of Object.entries(csp)) {
17
+ if (!Array.isArray(value)) continue
18
+
19
+ const valueFiltered = value.filter((x) => x !== "'none'")
20
+
21
+ if (valueFiltered.length) {
22
+ ;(csp as Record<string, unknown>)[key] = [...new Set(valueFiltered)]
23
+ }
24
+ }
25
+ }
26
+
27
+ return nuxtSecurityConfiguration
28
+ }
29
+
30
+ export default defineNitroPlugin((nitroApp) => {
31
+ // TODO: migrate to nuxt-security (https://github.com/Baroshem/nuxt-security/discussions/454)
32
+ if (import.meta.dev) {
33
+ nitroApp.hooks.hook('render:html', (html, { event }) => {
34
+ html.head.push(
35
+ `<meta property="csp-nonce" nonce="${event.context.security.nonce}">`,
36
+ )
37
+ })
38
+ }
39
+
40
+ nitroApp.hooks.hook('nuxt-security:routeRules', async (routeRules) => {
41
+ const runtimeConfig = useRuntimeConfig()
42
+ const siteUrl = runtimeConfig.public.site.url
43
+
44
+ routeRules['/**'] = cleanupCsp(
45
+ defu(
46
+ {
47
+ headers: {
48
+ contentSecurityPolicy: GET_CSP(siteUrl),
49
+ },
50
+ },
51
+ routeRules['/**'],
52
+ ),
53
+ )
54
+ })
55
+ })
@@ -0,0 +1,32 @@
1
+ import type { H3Event } from 'h3'
2
+
3
+ import { TIMEZONE_COOKIE_NAME } from '../../utils/constants'
4
+
5
+ // TODO: rename to `getTimezone` (https://github.com/nuxt/cli/issues/266)
6
+ export const getTimezoneServer = async (event: H3Event) => {
7
+ const timezoneBySsr = event.context.$timezone
8
+
9
+ if (timezoneBySsr) return timezoneBySsr
10
+
11
+ const timezoneByCookie = getCookie(event, TIMEZONE_COOKIE_NAME)
12
+
13
+ if (timezoneByCookie) return timezoneByCookie
14
+
15
+ const ip = event.node.req.headers['x-real-ip']
16
+
17
+ if (ip && !Array.isArray(ip)) {
18
+ const timezoneByIpApi = await getTimezoneByIpApi(ip)
19
+
20
+ if (timezoneByIpApi) return timezoneByIpApi
21
+ }
22
+ }
23
+
24
+ export const getTimezoneByIpApi = async (ip: string) => {
25
+ const ipApiResult = await $fetch<{ timezone: string }>(
26
+ `http://ip-api.com/json/${ip}`,
27
+ ).catch(() => {})
28
+
29
+ if (ipApiResult) {
30
+ return ipApiResult.timezone
31
+ }
32
+ }
@@ -0,0 +1,10 @@
1
+ export const getTimezone = () =>
2
+ useNuxtApp().ssrContext?.event.context.$timezone ||
3
+ useCookie(TIMEZONE_COOKIE_NAME, {
4
+ httpOnly: false,
5
+ sameSite: 'strict',
6
+ secure: !import.meta.dev,
7
+ }).value ||
8
+ import.meta.client
9
+ ? Intl.DateTimeFormat().resolvedOptions().timeZone
10
+ : undefined
@@ -1,4 +1,5 @@
1
1
  import { helpers } from '@vuelidate/validators'
2
+ import { defu } from 'defu'
2
3
 
3
4
  export const SITE_NAME = 'Vio'
4
5
 
@@ -13,6 +14,115 @@ export const CACHE_VERSION = 'bOXMwoKlJr'
13
14
  export const COOKIE_PREFIX = SITE_NAME.toLocaleLowerCase()
14
15
  export const COOKIE_SEPARATOR = '_'
15
16
  export const FETCH_RETRY_AMOUNT = 3
17
+ export const GET_CSP = (siteUrl: string) =>
18
+ defu(
19
+ {
20
+ // Cloudflare
21
+ ...(process.env.NODE_ENV === 'production'
22
+ ? {
23
+ 'connect-src': ['https://cloudflareinsights.com'],
24
+ 'script-src-elem': ['https://static.cloudflareinsights.com'],
25
+ }
26
+ : {}),
27
+ },
28
+ {
29
+ // Google Analytics 4 (https://developers.google.com/tag-platform/tag-manager/web/csp)
30
+ 'connect-src': [
31
+ 'https://*.analytics.google.com',
32
+ 'https://*.google-analytics.com',
33
+ 'https://*.googletagmanager.com',
34
+ ],
35
+ 'img-src': [
36
+ 'https://*.google-analytics.com',
37
+ 'https://*.googletagmanager.com',
38
+ ],
39
+ 'script-src-elem': ['https://*.googletagmanager.com'],
40
+ },
41
+ {
42
+ // vio
43
+ 'manifest-src': [`${siteUrl}/site.webmanifest`],
44
+ // 'script-src-elem': [
45
+ // 'https://polyfill.io/v3/polyfill.min.js', // ESLint plugin compat
46
+ // ],
47
+ },
48
+ {
49
+ // nuxt-link-checker
50
+ ...(process.env.NODE_ENV === 'development'
51
+ ? {
52
+ 'connect-src': [`${siteUrl}/api/__link_checker__/inspect`],
53
+ }
54
+ : {}),
55
+ },
56
+ {
57
+ // nuxt-og-image
58
+ ...(process.env.NODE_ENV === 'development'
59
+ ? {
60
+ 'connect-src': [`${siteUrl}/__og-image__/`],
61
+ }
62
+ : {}),
63
+ },
64
+ {
65
+ // nuxt-schema-org
66
+ ...(process.env.NODE_ENV === 'development'
67
+ ? {
68
+ 'connect-src': [`${siteUrl}/__schema-org__/debug.json`],
69
+ }
70
+ : {}),
71
+ },
72
+ {
73
+ // nuxt-simple-robots
74
+ ...(process.env.NODE_ENV === 'development'
75
+ ? {
76
+ 'connect-src': [
77
+ `${siteUrl}/__robots__/debug.json`,
78
+ `${siteUrl}/__robots__/debug-path.json`,
79
+ ],
80
+ }
81
+ : {}),
82
+ },
83
+ {
84
+ // nuxt-simple-sitemap
85
+ ...(process.env.NODE_ENV === 'development'
86
+ ? {
87
+ 'connect-src': [`${siteUrl}/__sitemap__/debug.json`],
88
+ }
89
+ : {}),
90
+ },
91
+ {
92
+ // nuxt-site-config
93
+ ...(process.env.NODE_ENV === 'development'
94
+ ? {
95
+ 'connect-src': [`${siteUrl}/__site-config__/debug.json`],
96
+ }
97
+ : {}),
98
+ },
99
+ {
100
+ // nuxt
101
+ 'connect-src': [
102
+ "'self'", // e.g. `/_nuxt/builds/meta/`, `/_payload.json`, `/privacy-policy/_payload.json`
103
+ ...(process.env.NODE_ENV === 'development'
104
+ ? [
105
+ 'http://localhost:3000/_nuxt/', // hot reload
106
+ 'https://localhost:3000/_nuxt/', // hot reload
107
+ 'ws://localhost:3000/_nuxt/', // hot reload
108
+ 'wss://localhost:3000/_nuxt/', // hot reload
109
+ ] // TODO: generalize for different ports
110
+ : []),
111
+ ],
112
+ 'img-src': [
113
+ "'self'", // e.g. favicon
114
+ 'data:', // external link icon
115
+ ],
116
+ 'script-src-elem': [
117
+ "'nonce-{{nonce}}'",
118
+ `${siteUrl}/_nuxt/`, // bundle
119
+ ],
120
+ 'style-src': [
121
+ "'nonce-{{nonce}}'",
122
+ "'self'", // TODO: `${siteUrl}/_nuxt/`, // bundle
123
+ ], // TODO: use `style-src-elem` once Playwright WebKit supports it
124
+ },
125
+ )
16
126
  export const GTAG_COOKIE_ID = 'ga'
17
127
  export const I18N_MODULE_CONFIG = {
18
128
  langDir: 'locales',
@@ -1,10 +1,9 @@
1
1
  import type { CombinedError } from '@urql/core'
2
- import { type H3Event, getCookie } from 'h3'
2
+ import { type H3Event } from 'h3'
3
3
 
4
4
  import { type Ref } from 'vue'
5
5
 
6
6
  import type { ApiData, BackendError } from '../types/api'
7
- import { TIMEZONE_COOKIE_NAME } from './constants'
8
7
 
9
8
  export const getApiDataDefault = (): ApiData =>
10
9
  computed(() =>
@@ -109,23 +108,3 @@ export const getServiceHref = ({
109
108
  return `https://${nameSubdomainString}${getDomainTldPort(host)}`
110
109
  }
111
110
  }
112
-
113
- export const getTimezone = async (event: H3Event) => {
114
- const timezoneCookie = getCookie(event, TIMEZONE_COOKIE_NAME)
115
-
116
- if (timezoneCookie) {
117
- return timezoneCookie
118
- }
119
-
120
- if (event.node.req.headers['x-real-ip']) {
121
- const ipApiResult = await $fetch<{ timezone: string }>(
122
- `http://ip-api.com/json/${event.node.req.headers['x-real-ip']}`,
123
- ).catch(() => {})
124
-
125
- if (ipApiResult) {
126
- return ipApiResult.timezone
127
- }
128
- }
129
-
130
- return undefined
131
- }
@@ -1,15 +0,0 @@
1
- import type { Dayjs } from 'dayjs'
2
-
3
- export const useDateTime = () => {
4
- const { $dayjs, ssrContext } = useNuxtApp()
5
- const timezoneCookie = useCookie(TIMEZONE_COOKIE_NAME)
6
-
7
- const timezoneHeader = ssrContext?.event.node.req.headers[TIMEZONE_HEADER_KEY]
8
- const timezone =
9
- timezoneHeader && !Array.isArray(timezoneHeader)
10
- ? timezoneHeader
11
- : timezoneCookie.value || undefined
12
-
13
- return (dateTime?: string | number | Dayjs | Date | null) =>
14
- $dayjs(dateTime).tz(timezone)
15
- }
@@ -1,2 +0,0 @@
1
- export { TIMEZONE_HEADER_KEY } from '../../utils/constants'
2
- export { getTimezone } from '../../utils/networking'