@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 +37 -54
- package/composables/dateTime.ts +11 -0
- package/composables/useAppLayout.ts +2 -2
- package/nuxt.config.ts +117 -190
- package/package.json +3 -3
- package/server/middleware/headers.ts +5 -23
- package/server/middleware/timezone.ts +9 -0
- package/server/plugins/security.ts +55 -0
- package/server/utils/timezone.ts +32 -0
- package/utils/autoImport.ts +10 -0
- package/utils/constants.ts +110 -0
- package/utils/networking.ts +1 -22
- package/composables/useDateTime.ts +0 -15
- package/server/utils/util.ts +0 -2
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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/
|
14
|
-
|
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
|
56
|
+
const nuxtConfigSecurityHeaders = nuxt.options.security.headers
|
54
57
|
|
55
58
|
if (
|
56
|
-
typeof
|
57
|
-
|
58
|
-
typeof nuxtConfigSecurity.headers.contentSecurityPolicy !==
|
59
|
-
'boolean' &&
|
60
|
-
typeof nuxtConfigSecurity.headers.contentSecurityPolicy !== 'string'
|
59
|
+
typeof nuxtConfigSecurityHeaders !== 'boolean' &&
|
60
|
+
nuxtConfigSecurityHeaders.contentSecurityPolicy
|
61
61
|
) {
|
62
|
-
|
63
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
91
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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:
|
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:
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
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": "
|
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 {
|
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
|
-
|
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
|
-
|
23
|
-
|
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,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
|
package/utils/constants.ts
CHANGED
@@ -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',
|
package/utils/networking.ts
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
import type { CombinedError } from '@urql/core'
|
2
|
-
import { type H3Event
|
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
|
-
}
|
package/server/utils/util.ts
DELETED