@dargmuesli/nuxt-vio 23.0.0-beta.1 → 23.0.0-beta.10

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.
@@ -0,0 +1,2 @@
1
+ *.crt
2
+ *.key
File without changes
package/app/app.config.ts CHANGED
@@ -4,63 +4,3 @@ export default defineAppConfig({
4
4
  themeColor: undefined,
5
5
  },
6
6
  })
7
-
8
- declare module 'nuxt/schema' {
9
- interface AppConfig {
10
- vio: {
11
- seo?: {
12
- ogImage?: {
13
- defaultComponent: string // TODO: keyof OgImageComponents
14
- }
15
- }
16
- pages?: {
17
- legalNotice?: {
18
- contact: {
19
- email: string
20
- }
21
- responsibility: {
22
- address: {
23
- city: string
24
- name: string
25
- street: string
26
- }
27
- }
28
- tmg: {
29
- address: {
30
- city: string
31
- name: string
32
- street: string
33
- }
34
- }
35
- }
36
- privacyPolicy?: {
37
- hostingCdn?: {
38
- external: {
39
- address: {
40
- city: string
41
- name: string
42
- street: string
43
- }
44
- }
45
- }
46
- mandatoryInfo?: {
47
- responsible: {
48
- address: {
49
- city: string
50
- email: string
51
- name: string
52
- street: string
53
- }
54
- }
55
- }
56
- }
57
- }
58
- server?: {
59
- middleware: {
60
- headers: Record<string, string>
61
- }
62
- }
63
- themeColor?: string
64
- }
65
- }
66
- }
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <VioButtonColored
3
3
  :aria-label="t('home')"
4
- :to="localePath('/')"
4
+ :to="localePath({ path: '/' })"
5
5
  @click="emit('click')"
6
6
  >
7
7
  {{ t('home') }}
@@ -4,7 +4,7 @@
4
4
  :aria-label="t('home')"
5
5
  data-testid="breadcrumb-prefix-/"
6
6
  :is-colored="false"
7
- :to="localePath('/')"
7
+ :to="localePath({ path: '/' })"
8
8
  >
9
9
  <VioIconHome classes="h-6 w-6" />
10
10
  </VioLink>
@@ -0,0 +1,17 @@
1
+ import { getSharedSentryConfig } from '../../node/static'
2
+
3
+ export const useSharedSentryConfig = () => {
4
+ const isTesting = useIsTesting()
5
+ const runtimeConfig = useRuntimeConfig()
6
+
7
+ return getSharedSentryConfig({
8
+ enableLogs: runtimeConfig.public.sentry.logs.enable,
9
+ environment: runtimeConfig.public.sentry.environment,
10
+ host: runtimeConfig.public.sentry.host,
11
+ isInProduction: runtimeConfig.public.vio.isInProduction,
12
+ isTesting,
13
+ projectId: runtimeConfig.public.sentry.project.id,
14
+ projectPublicKey: runtimeConfig.public.sentry.project.publicKey,
15
+ release: runtimeConfig.public.sentry.release,
16
+ })
17
+ }
@@ -1,9 +1,11 @@
1
+ import * as Sentry from '@sentry/nuxt'
1
2
  import { decodeJwt } from 'jose'
2
3
  import type { JWTPayload } from 'jose'
3
4
  import { defineStore } from 'pinia'
4
5
  import { ref } from 'vue'
5
6
 
6
7
  export const useVioAuthStore = defineStore('vio-auth', () => {
8
+ const runtimeConfig = useRuntimeConfig()
7
9
  const jwt = ref<string>()
8
10
  const jwtDecoded = ref<JWTPayload>()
9
11
  const signedInUsername = ref<string>()
@@ -21,6 +23,17 @@ export const useVioAuthStore = defineStore('vio-auth', () => {
21
23
  jwtDecodedNew.exp > Math.floor(Date.now() / 1000)
22
24
  ? (jwtDecodedNew.username as string | undefined)
23
25
  : undefined
26
+
27
+ if (runtimeConfig.public.sentry.features.userTracking) {
28
+ Sentry.setUser(
29
+ signedInUsername.value
30
+ ? {
31
+ id: jwtDecodedNew?.sub as string | undefined,
32
+ username: signedInUsername.value,
33
+ }
34
+ : null,
35
+ )
36
+ }
24
37
  }
25
38
 
26
39
  return {
@@ -1,11 +1,9 @@
1
- import type { H3Event } from 'h3'
2
-
3
1
  import { SERVICES } from './services'
4
2
  import type { Service, ServiceName } from './services'
5
3
  import { SITE_URL_TYPED } from './static'
6
4
 
7
- export const getHost = (event: H3Event) => {
8
- const host = event.node.req.headers.host
5
+ export const getHost = (event: { headers: Headers }) => {
6
+ const host = event.headers.get('host')
9
7
 
10
8
  if (!host) throw new Error('Host header is not given!')
11
9
 
@@ -16,11 +14,16 @@ export const getRootHost = (host: string) => {
16
14
  const hostParts = host.split('.')
17
15
  const hostPartsLast = hostParts[hostParts.length - 1]
18
16
 
19
- if (hostPartsLast && /^localhost(:[0-9]+)?$/.test(hostPartsLast))
20
- return hostPartsLast
21
-
22
17
  if (hostParts.length === 1) return hostParts[0]
23
18
 
19
+ // only a single subdomain directly on bare localhost collapses to the root; e.g. app.localhost must keep its "app" label
20
+ if (
21
+ hostParts.length === 2 &&
22
+ hostPartsLast &&
23
+ /^localhost(:[0-9]+)?$/.test(hostPartsLast)
24
+ )
25
+ return hostPartsLast
26
+
24
27
  return `${hostParts[hostParts.length - 2]}.${hostPartsLast}`
25
28
  }
26
29
 
@@ -1,2 +1,3 @@
1
1
  export * from './environment'
2
2
  export * from './nuxt'
3
+ export * from './sentry'
@@ -0,0 +1,42 @@
1
+ import type * as Sentry from '@sentry/nuxt'
2
+
3
+ export const NUXT_PUBLIC_SENTRY_ENVIRONMENT =
4
+ process.env.NUXT_PUBLIC_SENTRY_ENVIRONMENT
5
+ export const NUXT_PUBLIC_SENTRY_HOST = process.env.NUXT_PUBLIC_SENTRY_HOST || ''
6
+ export const NUXT_PUBLIC_SENTRY_LOGS_ENABLE =
7
+ process.env.NUXT_PUBLIC_SENTRY_LOGS_ENABLE !== 'false'
8
+ export const NUXT_PUBLIC_SENTRY_PROJECT_ID =
9
+ process.env.NUXT_PUBLIC_SENTRY_PROJECT_ID || ''
10
+ export const NUXT_PUBLIC_SENTRY_PROJECT_PUBLIC_KEY =
11
+ process.env.NUXT_PUBLIC_SENTRY_PROJECT_PUBLIC_KEY || ''
12
+ export const NUXT_PUBLIC_SENTRY_RELEASE = process.env.RELEASE_NAME
13
+
14
+ export const getSharedSentryConfig = ({
15
+ enableLogs,
16
+ environment,
17
+ host,
18
+ isInProduction,
19
+ isTesting,
20
+ projectId,
21
+ projectPublicKey,
22
+ release,
23
+ }: {
24
+ enableLogs?: boolean
25
+ environment?: string
26
+ host?: string
27
+ isInProduction: boolean
28
+ isTesting?: boolean
29
+ projectId?: string
30
+ projectPublicKey?: string
31
+ release?: string
32
+ }): Parameters<typeof Sentry.init>[0] => ({
33
+ dsn:
34
+ projectPublicKey && host && projectId
35
+ ? `https://${projectPublicKey}@${host}/${projectId}`
36
+ : undefined,
37
+ enabled: isInProduction && !isTesting,
38
+ enableLogs,
39
+ environment,
40
+ release,
41
+ tracesSampleRate: 1.0,
42
+ })
package/nuxt.config.ts CHANGED
@@ -2,7 +2,17 @@ import tailwindcss from '@tailwindcss/vite'
2
2
  import { defu } from 'defu'
3
3
  import { createResolver } from 'nuxt/kit'
4
4
 
5
- import { IS_IN_STACK, SITE_URL, VIO_NUXT_BASE_CONFIG } from './node/static'
5
+ import {
6
+ IS_IN_STACK,
7
+ NUXT_PUBLIC_SENTRY_ENVIRONMENT,
8
+ NUXT_PUBLIC_SENTRY_HOST,
9
+ NUXT_PUBLIC_SENTRY_LOGS_ENABLE,
10
+ NUXT_PUBLIC_SENTRY_PROJECT_ID,
11
+ NUXT_PUBLIC_SENTRY_PROJECT_PUBLIC_KEY,
12
+ NUXT_PUBLIC_SENTRY_RELEASE,
13
+ SITE_URL,
14
+ VIO_NUXT_BASE_CONFIG,
15
+ } from './node/static'
6
16
  import {
7
17
  GTAG_COOKIE_ID,
8
18
  TIMEZONE_COOKIE_NAME,
@@ -11,6 +21,10 @@ import {
11
21
  } from './shared/utils/constants'
12
22
 
13
23
  const { resolve } = createResolver(import.meta.url)
24
+ const SHARED_TYPE_REFERENCES = [
25
+ { path: resolve('shared/types/app-config.d.ts') },
26
+ { path: resolve('shared/types/request-event-context.d.ts') },
27
+ ]
14
28
 
15
29
  export default defineNuxtConfig(
16
30
  defu(
@@ -42,6 +56,25 @@ export default defineNuxtConfig(
42
56
  enabled: true,
43
57
  },
44
58
  },
59
+ experimental: {
60
+ early404: true,
61
+ inlineErrorRendering: true,
62
+ prerenderErrorPages: true,
63
+ routeTypedFetch: true,
64
+ strictRouteTypes: true,
65
+ typedPages: true,
66
+ },
67
+ hooks: {
68
+ // consumers' generated tsconfigs exclude `node_modules`, so this layer's type augmentations need explicit references
69
+ 'prepare:types'({ nodeReferences, references, sharedReferences }) {
70
+ for (const list of [nodeReferences, references, sharedReferences]) {
71
+ list.push(...SHARED_TYPE_REFERENCES)
72
+ }
73
+ },
74
+ 'nitro:prepare:types'({ references }) {
75
+ references.push(...SHARED_TYPE_REFERENCES)
76
+ },
77
+ },
45
78
  modules: [
46
79
  '@dargmuesli/nuxt-cookie-control',
47
80
  '@nuxt/devtools',
@@ -53,6 +86,7 @@ export default defineNuxtConfig(
53
86
  '@nuxtjs/seo',
54
87
  '@nuxtjs/turnstile',
55
88
  '@pinia/nuxt',
89
+ '@sentry/nuxt/module',
56
90
  'nuxt-gtag',
57
91
  'shadcn-nuxt',
58
92
  (_options, nuxt) => {
@@ -72,7 +106,16 @@ export default defineNuxtConfig(
72
106
  ) {
73
107
  if (nuxt.options.nitro.static) {
74
108
  nuxtConfigSecurityHeaders.contentSecurityPolicy = defu(
75
- VIO_GET_CSP({ siteUrl: new URL(SITE_URL) }),
109
+ VIO_GET_CSP({
110
+ sentry:
111
+ NUXT_PUBLIC_SENTRY_HOST && NUXT_PUBLIC_SENTRY_PROJECT_ID
112
+ ? {
113
+ host: NUXT_PUBLIC_SENTRY_HOST,
114
+ projectId: NUXT_PUBLIC_SENTRY_PROJECT_ID,
115
+ }
116
+ : undefined,
117
+ siteUrl: new URL(SITE_URL),
118
+ }),
76
119
  nuxtConfigSecurityHeaders.contentSecurityPolicy,
77
120
  )
78
121
  }
@@ -98,7 +141,6 @@ export default defineNuxtConfig(
98
141
  compressPublicAssets: true,
99
142
  experimental: {
100
143
  asyncContext: true,
101
- typedPages: true,
102
144
  },
103
145
  },
104
146
  runtimeConfig: {
@@ -106,6 +148,37 @@ export default defineNuxtConfig(
106
148
  i18n: {
107
149
  baseUrl: SITE_URL,
108
150
  },
151
+ sentry: {
152
+ environment: NUXT_PUBLIC_SENTRY_ENVIRONMENT,
153
+ features: {
154
+ consoleCapture: true,
155
+ pinia: true,
156
+ profiling: true,
157
+ replay: true,
158
+ securityHeaders: true,
159
+ userTracking: true,
160
+ },
161
+ host: NUXT_PUBLIC_SENTRY_HOST,
162
+ logs: {
163
+ enable: NUXT_PUBLIC_SENTRY_LOGS_ENABLE,
164
+ },
165
+ profiles: {
166
+ sampleRate: 1.0,
167
+ },
168
+ project: {
169
+ id: NUXT_PUBLIC_SENTRY_PROJECT_ID,
170
+ publicKey: NUXT_PUBLIC_SENTRY_PROJECT_PUBLIC_KEY,
171
+ },
172
+ release: NUXT_PUBLIC_SENTRY_RELEASE,
173
+ replays: {
174
+ onError: {
175
+ sampleRate: 1.0,
176
+ },
177
+ session: {
178
+ sampleRate: 0.0,
179
+ },
180
+ },
181
+ },
109
182
  site: {
110
183
  url: SITE_URL, // TODO: evaluate removal for static builds in favor of i18n.baseUrl
111
184
  },
@@ -122,24 +195,19 @@ export default defineNuxtConfig(
122
195
  },
123
196
  },
124
197
  typescript: {
125
- nodeTsConfig: {
126
- include: [resolve('./node')],
127
- },
128
- tsConfig: {
129
- nodeTsConfig: {
130
- include: [
131
- resolve('../.config'),
132
- resolve('../node'),
133
- // resolve('../sentry.server.config.ts'),
134
- ],
135
- },
198
+ appTsConfig: {
136
199
  vueCompilerOptions: {
137
200
  htmlAttributes: [], // https://github.com/johnsoncodehk/volar/issues/1970#issuecomment-1276994634
138
201
  },
139
- // compilerOptions: {
140
- // noErrorTruncation: true,
141
- // },
142
202
  },
203
+ nodeTsConfig: {
204
+ include: [resolve('./node')],
205
+ },
206
+ // tsConfig: {
207
+ // compilerOptions: {
208
+ // noErrorTruncation: true,
209
+ // },
210
+ // },
143
211
  },
144
212
  vite: {
145
213
  plugins: [
@@ -290,8 +358,19 @@ export default defineNuxtConfig(
290
358
  },
291
359
  strict: true,
292
360
  },
361
+ sentry: {
362
+ release: {
363
+ name: process.env.RELEASE_NAME || undefined,
364
+ },
365
+ sourcemaps: {
366
+ disable: !process.env.SENTRY_AUTH_TOKEN,
367
+ },
368
+ telemetry: false,
369
+ // `org`, `project` and `authToken` are read from the `SENTRY_ORG`, `SENTRY_PROJECT` and `SENTRY_AUTH_TOKEN` environment variables by the module itself.
370
+ },
293
371
  seo: {
294
372
  minify: false, // TODO: enable (https://github.com/harlan-zw/nuxt-seo-utils/issues/103)
373
+ validateAppHead: false, // `app.head.htmlAttrs.lang` is only a fallback for the routeless SPA pages, as i18n sets the correct language per locale everywhere else.
295
374
  },
296
375
  shadcn: {
297
376
  prefix: '',
@@ -301,6 +380,7 @@ export default defineNuxtConfig(
301
380
  credits: false,
302
381
  zeroRuntime: true,
303
382
  },
383
+ skewProtection: false, // needs storage that persists across builds (https://nuxtseo.com/docs/skew-protection/guides/storage-configuration), which docker builds in ci do not provide
304
384
 
305
385
  // environments
306
386
  $development: {
@@ -326,6 +406,9 @@ export default defineNuxtConfig(
326
406
  strictTransportSecurity: false, // prevent endless reload in Chrome
327
407
  },
328
408
  },
409
+ sentry: {
410
+ enabled: false,
411
+ },
329
412
  site: {
330
413
  debug: true,
331
414
  },
package/package.json CHANGED
@@ -1,80 +1,81 @@
1
1
  {
2
2
  "dependencies": {
3
- "@dargmuesli/nuxt-cookie-control": "9.3.3",
4
- "@eslint/compat": "2.1.0",
3
+ "@dargmuesli/nuxt-cookie-control": "9.3.8",
4
+ "@eslint/compat": "2.1.1",
5
5
  "@heroicons/vue": "2.2.0",
6
- "@http-util/status-i18n": "0.9.0",
6
+ "@http-util/status-i18n": "0.9.1",
7
7
  "@intlify/eslint-plugin-vue-i18n": "4.5.1",
8
- "@lucide/vue": "1.33.0",
9
- "@nuxt/devtools": "3.4.1",
8
+ "@lucide/vue": "1.53.0",
9
+ "@nuxt/devtools": "3.4.2",
10
10
  "@nuxt/eslint": "1.17.0",
11
11
  "@nuxt/image": "2.1.0",
12
12
  "@nuxtjs/color-mode": "4.0.1",
13
13
  "@nuxtjs/html-validator": "2.1.0",
14
14
  "@nuxtjs/i18n": "10.6.0",
15
- "@nuxtjs/seo": "5.3.12",
15
+ "@nuxtjs/seo": "6.0.1",
16
16
  "@nuxtjs/turnstile": "1.1.3",
17
17
  "@pinia/nuxt": "1.0.2",
18
+ "@sentry/nuxt": "10.71.0",
18
19
  "@tailwindcss/forms": "0.5.11",
19
20
  "@tailwindcss/typography": "0.5.20",
20
21
  "@tailwindcss/vite": "4.3.3",
21
- "@takumi-rs/core": "2.10.0",
22
+ "@takumi-rs/core": "2.15.1",
22
23
  "@types/lodash-es": "4.17.12",
23
- "@types/nodemailer": "8.0.1",
24
+ "@types/nodemailer": "8.0.2",
24
25
  "@urql/core": "6.0.3",
25
26
  "@urql/vue": "2.1.1",
26
27
  "@vuelidate/core": "2.0.3",
27
28
  "@vuelidate/validators": "2.0.4",
28
- "@vueuse/core": "14.4.0",
29
+ "@vueuse/core": "15.0.0",
29
30
  "class-variance-authority": "0.7.1",
30
31
  "clsx": "2.1.1",
31
- "eslint": "10.8.1",
32
+ "eslint": "10.12.0",
32
33
  "eslint-config-prettier": "10.1.8",
33
34
  "eslint-plugin-compat": "7.0.2",
34
35
  "eslint-plugin-prettier": "5.5.6",
35
- "eslint-plugin-yml": "3.8.1",
36
- "globals": "17.11.0",
36
+ "eslint-plugin-yml": "3.9.0",
37
+ "globals": "17.13.0",
37
38
  "jiti": "2.7.0",
38
- "jose": "6.2.9",
39
- "nodemailer": "9.0.5",
39
+ "jose": "6.2.12",
40
+ "nodemailer": "10.0.16",
40
41
  "nuxt-gtag": "5.0.0",
41
42
  "nuxt-security": "2.6.0",
42
- "reka-ui": "2.10.3",
43
+ "reka-ui": "2.11.0",
43
44
  "shadcn-nuxt": "2.8.2",
44
- "tailwind-merge": "3.6.0",
45
+ "tailwind-merge": "3.7.0",
45
46
  "tw-animate-css": "1.4.0",
46
47
  "typescript": "6.0.3",
47
48
  "vue-sonner": "2.0.9",
48
- "vue-tsc": "3.3.10"
49
+ "vue-tsc": "3.3.11"
49
50
  },
50
51
  "devDependencies": {
51
- "@types/node": "24.13.3",
52
+ "@types/node": "24.19.1",
52
53
  "@urql/devtools": "2.0.3",
53
- "@urql/exchange-graphcache": "9.0.1",
54
- "@vueuse/core": "14.4.0",
54
+ "@urql/exchange-graphcache": "9.0.2",
55
+ "@vueuse/core": "15.0.0",
55
56
  "consola": "3.4.2",
56
57
  "defu": "6.1.7",
57
- "h3": "1.15.11",
58
58
  "lodash-es": "4.18.1",
59
- "nuxt": "4.5.2",
59
+ "nuxt": "4.6.1",
60
+ "nuxtseo-devtools-host": "1.0.1",
60
61
  "pinia": "4.0.3",
61
62
  "postcss-html": "2.0.0",
62
- "prettier": "3.9.6",
63
+ "prettier": "3.9.9",
63
64
  "prettier-plugin-tailwindcss": "0.8.1",
64
65
  "serve": "14.2.6",
65
- "sharp": "0.35.3",
66
- "stylelint": "17.14.1",
66
+ "sharp": "0.35.5",
67
+ "stylelint": "17.16.0",
67
68
  "stylelint-config-html": "2.0.0",
68
69
  "stylelint-config-recommended": "18.0.0",
69
70
  "stylelint-config-recommended-vue": "2.0.0",
70
71
  "stylelint-config-standard": "40.0.0",
71
- "stylelint-no-unsupported-browser-features": "8.1.1",
72
+ "stylelint-no-unsupported-browser-features": "8.1.2",
72
73
  "tailwindcss": "4.3.3",
73
- "vue": "3.5.41",
74
- "vue-router": "5.2.0"
74
+ "vue": "3.5.43",
75
+ "vue-router": "5.4.0"
75
76
  },
76
77
  "engines": {
77
- "node": "24"
78
+ "node": "^24.15.0"
78
79
  },
79
80
  "files": [
80
81
  ".config",
@@ -84,12 +85,14 @@
84
85
  "server",
85
86
  "shared",
86
87
  "nuxt.config.ts",
87
- "package.json"
88
+ "package.json",
89
+ "sentry.client.config.ts",
90
+ "sentry.server.config.ts"
88
91
  ],
89
92
  "main": "nuxt.config.ts",
90
93
  "name": "@dargmuesli/nuxt-vio",
91
94
  "peerDependencies": {
92
- "nuxt": "^4.1.0",
95
+ "nuxt": "^4.6.0",
93
96
  "vue": "^3.5.21",
94
97
  "vue-router": "^4.5.1 || ^5.0.0"
95
98
  },
@@ -101,7 +104,7 @@
101
104
  "url": "git+https://github.com/dargmuesli/vio.git"
102
105
  },
103
106
  "type": "module",
104
- "version": "23.0.0-beta.1",
107
+ "version": "23.0.0-beta.10",
105
108
  "scripts": {
106
109
  "build": "pnpm run build:node",
107
110
  "build:node": "NUXT_PUBLIC_I18N_BASE_URL=https://app.localhost:3001 nuxt build playground",
@@ -0,0 +1,38 @@
1
+ import * as Sentry from '@sentry/nuxt'
2
+
3
+ const runtimeConfig = useRuntimeConfig()
4
+ const sharedSentryConfig = useSharedSentryConfig()
5
+
6
+ if (sharedSentryConfig.dsn) {
7
+ const features = runtimeConfig.public.sentry.features
8
+
9
+ Sentry.init({
10
+ ...sharedSentryConfig,
11
+ integrations: [
12
+ ...(features.profiling ? [Sentry.browserProfilingIntegration()] : []),
13
+ ...(features.consoleCapture
14
+ ? [
15
+ Sentry.captureConsoleIntegration(),
16
+ Sentry.consoleLoggingIntegration(),
17
+ ]
18
+ : []),
19
+ Sentry.httpClientIntegration(),
20
+ ...(features.pinia ? [Sentry.piniaIntegration(usePinia())] : []),
21
+ ...(features.replay ? [Sentry.replayIntegration()] : []),
22
+ Sentry.zodErrorsIntegration(),
23
+ ],
24
+ profilesSampleRate: features.profiling
25
+ ? runtimeConfig.public.sentry.profiles.sampleRate
26
+ : undefined,
27
+ replaysOnErrorSampleRate: features.replay
28
+ ? runtimeConfig.public.sentry.replays.onError.sampleRate
29
+ : undefined,
30
+ replaysSessionSampleRate: features.replay
31
+ ? runtimeConfig.public.sentry.replays.session.sampleRate
32
+ : undefined,
33
+ })
34
+ } else {
35
+ console.warn(
36
+ 'Sentry configuration is incomplete, skipping Sentry initialization.',
37
+ )
38
+ }
@@ -0,0 +1,36 @@
1
+ import * as Sentry from '@sentry/nuxt'
2
+
3
+ import {
4
+ getSharedSentryConfig,
5
+ IS_IN_PRODUCTION,
6
+ NUXT_PUBLIC_SENTRY_ENVIRONMENT,
7
+ NUXT_PUBLIC_SENTRY_HOST,
8
+ NUXT_PUBLIC_SENTRY_LOGS_ENABLE,
9
+ NUXT_PUBLIC_SENTRY_PROJECT_ID,
10
+ NUXT_PUBLIC_SENTRY_PROJECT_PUBLIC_KEY,
11
+ NUXT_PUBLIC_SENTRY_RELEASE,
12
+ } from './node/static'
13
+
14
+ const sharedSentryConfig = getSharedSentryConfig({
15
+ enableLogs: NUXT_PUBLIC_SENTRY_LOGS_ENABLE,
16
+ environment: NUXT_PUBLIC_SENTRY_ENVIRONMENT,
17
+ host: NUXT_PUBLIC_SENTRY_HOST,
18
+ isInProduction: IS_IN_PRODUCTION,
19
+ isTesting: !!process.env.NUXT_PUBLIC_VIO_IS_TESTING,
20
+ projectId: NUXT_PUBLIC_SENTRY_PROJECT_ID,
21
+ projectPublicKey: NUXT_PUBLIC_SENTRY_PROJECT_PUBLIC_KEY,
22
+ release: NUXT_PUBLIC_SENTRY_RELEASE,
23
+ })
24
+
25
+ if (sharedSentryConfig.dsn) {
26
+ Sentry.init({
27
+ ...sharedSentryConfig,
28
+ integrations: NUXT_PUBLIC_SENTRY_LOGS_ENABLE
29
+ ? [Sentry.consoleLoggingIntegration()]
30
+ : [],
31
+ })
32
+ } else {
33
+ console.warn(
34
+ 'Sentry configuration is incomplete, skipping Sentry initialization.',
35
+ )
36
+ }
@@ -1,5 +1,6 @@
1
+ import { defineEventHandler } from 'nuxt/server'
2
+
1
3
  export default defineEventHandler((event) => {
2
- const { res } = event.node
3
- res.setHeader('Content-Type', 'text/plain')
4
- res.end('OK')
4
+ event.res.headers.set('Content-Type', 'text/plain')
5
+ return 'OK'
5
6
  })
@@ -1,8 +1,11 @@
1
+ import { defineEventHandler } from 'nuxt/server'
2
+
1
3
  export default defineEventHandler(async (event) => {
2
4
  const isTesting = useIsTesting()
3
5
  event.context.$timeZone = await getTimeZone({ event, isTesting })
4
6
  })
5
7
 
8
+ // h3 handlers, `useEvent()` and the app's `ssrContext.event` still see h3's context
6
9
  declare module 'h3' {
7
10
  interface H3EventContext {
8
11
  $timeZone?: string
@@ -1,9 +1,11 @@
1
- export default defineEventHandler(async (event) => {
1
+ import { defineEventHandler, useAppConfig } from 'nuxt/server'
2
+
3
+ export default defineEventHandler((event) => {
2
4
  const config = useAppConfig(event)
3
5
 
4
6
  if (!config.vio.server) return
5
7
 
6
8
  for (const entry of Object.entries(config.vio.server.middleware.headers)) {
7
- appendHeader(event, entry[0], entry[1])
9
+ event.res.headers.append(entry[0], entry[1])
8
10
  }
9
11
  })
@@ -0,0 +1,33 @@
1
+ export default defineEventHandler((event) => {
2
+ const runtimeConfig = useRuntimeConfig(event)
3
+ const sentryConfig = runtimeConfig.public.sentry
4
+
5
+ if (
6
+ !sentryConfig.features.securityHeaders ||
7
+ !sentryConfig.host ||
8
+ !sentryConfig.project.id ||
9
+ !sentryConfig.project.publicKey
10
+ ) {
11
+ return
12
+ }
13
+
14
+ const securityEndpointParams = [
15
+ `sentry_key=${sentryConfig.project.publicKey}`,
16
+ sentryConfig.environment &&
17
+ `sentry_environment=${sentryConfig.environment}`,
18
+ sentryConfig.release && `sentry_release=${sentryConfig.release}`,
19
+ ]
20
+ .filter(Boolean)
21
+ .join('&')
22
+
23
+ appendHeader(
24
+ event,
25
+ 'NEL',
26
+ '\'{"report_to":"sentry","max_age":31536000,"include_subdomains":true}\'',
27
+ )
28
+ appendHeader(
29
+ event,
30
+ 'Report-To',
31
+ `'{"group":"sentry","max_age":31536000,"endpoints":[{"url":"https://${sentryConfig.host}/api/${sentryConfig.project.id}/security/?${securityEndpointParams}"}],"include_subdomains":true}'`,
32
+ )
33
+ })
@@ -27,6 +27,7 @@ export default defineNitroPlugin((nitroApp) => {
27
27
  })
28
28
  })
29
29
 
30
+ // h3 handlers and `useEvent()` still see h3's context
30
31
  declare module 'h3' {
31
32
  interface H3EventContext {
32
33
  $email?: {
@@ -1,10 +1,10 @@
1
- import type { H3Event } from 'h3'
1
+ import { getCookie, getRequestIP, type RequestEvent } from 'nuxt/server'
2
2
 
3
3
  export const getTimeZone = async ({
4
4
  event,
5
5
  isTesting,
6
6
  }: {
7
- event: H3Event
7
+ event: RequestEvent
8
8
  isTesting?: boolean
9
9
  }) => {
10
10
  const timeZoneBySsr = event.context.$timeZone
@@ -26,12 +26,8 @@ export const getTimeZone = async ({
26
26
  }
27
27
  }
28
28
 
29
- export const useTimeZone = async () => {
30
- const event = useEvent()
31
- const isTesting = useIsTesting()
32
-
33
- return await getTimeZone({ event, isTesting })
34
- }
29
+ // the `dateTime` middleware has already resolved the time zone for every request
30
+ export const useTimeZone = async () => useEvent().context.$timeZone
35
31
 
36
32
  export const getTimeZoneByIpApi = async ({ ip }: { ip: string }) => {
37
33
  const ipApiResult = await $fetch<{ timezone: string }>(
@@ -1,4 +1,6 @@
1
+ import { verifyTurnstileToken } from '@nuxtjs/turnstile/runtime/server/utils/verify.js'
1
2
  import { consola } from 'consola'
3
+ import { createError } from 'nuxt/server'
2
4
 
3
5
  /**
4
6
  * Verifies a Turnstile token from the request body
@@ -2,6 +2,11 @@ export const useSiteUrl = () => {
2
2
  const runtimeConfig = useRuntimeConfig()
3
3
  const siteUrl = runtimeConfig.public.i18n.baseUrl
4
4
 
5
+ // consumers see the i18n module's wider type, which also allows a resolver function
6
+ if (typeof siteUrl !== 'string') {
7
+ throw new Error('`runtimeConfig.public.i18n.baseUrl` must be a string')
8
+ }
9
+
5
10
  return {
6
11
  siteUrl,
7
12
  siteUrlTyped: new URL(siteUrl),
@@ -1,5 +1,15 @@
1
1
  import type { H3Event } from 'h3'
2
2
 
3
+ // `useEvent()` throws whenever there is no Nitro request context, which is the case for everything that runs at startup, such as a Nitro plugin's hooks.
4
+ // Testing detection still works there through the runtime config, so fall back to no event rather than letting that error escape.
5
+ const tryUseEvent = () => {
6
+ try {
7
+ return useEvent()
8
+ } catch {
9
+ return undefined
10
+ }
11
+ }
12
+
3
13
  export const useIsTesting = ({
4
14
  isCookieEnabled = true,
5
15
  }:
@@ -7,7 +17,7 @@ export const useIsTesting = ({
7
17
  isCookieEnabled?: boolean
8
18
  }
9
19
  | undefined = {}) => {
10
- const event = useEvent()
20
+ const event = tryUseEvent()
11
21
  const runtimeConfig = useRuntimeConfig()
12
22
 
13
23
  return getIsTesting({ event, isCookieEnabled, runtimeConfig })
@@ -0,0 +1,62 @@
1
+ // in `shared/` so that both the app's and the server's `useAppConfig` are typed
2
+ declare module '@nuxt/schema' {
3
+ interface CustomAppConfig {
4
+ vio: {
5
+ seo?: {
6
+ ogImage?: {
7
+ defaultComponent: string // TODO: keyof OgImageComponents
8
+ }
9
+ }
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
27
+ }
28
+ }
29
+ }
30
+ privacyPolicy?: {
31
+ hostingCdn?: {
32
+ external: {
33
+ address: {
34
+ city: string
35
+ name: string
36
+ street: string
37
+ }
38
+ }
39
+ }
40
+ mandatoryInfo?: {
41
+ responsible: {
42
+ address: {
43
+ city: string
44
+ email: string
45
+ name: string
46
+ street: string
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ server?: {
53
+ middleware: {
54
+ headers: Record<string, string>
55
+ }
56
+ }
57
+ themeColor?: string
58
+ }
59
+ }
60
+ }
61
+
62
+ export {}
@@ -0,0 +1,13 @@
1
+ import type { Transporter } from 'nodemailer'
2
+
3
+ // in `shared/` because typed `$fetch` also type-checks server handlers in the app context
4
+ declare module '@nuxt/schema' {
5
+ interface RequestEventContext {
6
+ $email?: {
7
+ nodemailer: {
8
+ transporter: Transporter
9
+ }
10
+ }
11
+ $timeZone?: string
12
+ }
13
+ }
@@ -12,7 +12,13 @@ export const COOKIE_CONTROL_CONSENT_COOKIE_NAME =
12
12
  export const COOKIE_PREFIX = VIO_SITE_NAME.toLocaleLowerCase()
13
13
  export const COOKIE_SEPARATOR = '_'
14
14
  export const FETCH_RETRY_AMOUNT = 3
15
- export const VIO_GET_CSP = ({ siteUrl }: { siteUrl: URL }) =>
15
+ export const VIO_GET_CSP = ({
16
+ sentry,
17
+ siteUrl,
18
+ }: {
19
+ sentry?: { host: string; projectId: string }
20
+ siteUrl: URL
21
+ }) =>
16
22
  defu(
17
23
  {
18
24
  // Cloudflare
@@ -26,6 +32,17 @@ export const VIO_GET_CSP = ({ siteUrl }: { siteUrl: URL }) =>
26
32
  }
27
33
  : {}),
28
34
  },
35
+ {
36
+ // Sentry
37
+ ...(sentry
38
+ ? {
39
+ 'connect-src': [
40
+ `https://${sentry.host}/api/${sentry.projectId}/envelope/`,
41
+ ],
42
+ 'worker-src': ['blob:'],
43
+ }
44
+ : {}),
45
+ },
29
46
  {
30
47
  // Google Analytics 4 (https://developers.google.com/tag-platform/tag-manager/web/csp)
31
48
  'connect-src': [
@@ -1,7 +0,0 @@
1
- export default defineNitroPlugin((_nitroApp) => {
2
- const runtimeConfig = useRuntimeConfig()
3
-
4
- if (runtimeConfig.public.vio.isTesting) {
5
- process.env.NUXT_OG_IMAGE_SECRET = 'test'
6
- }
7
- })