@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 +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