@dargmuesli/nuxt-vio 11.2.6 → 12.0.1

Sign up to get free protection for your applications and to get access to all the features.
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'