@dargmuesli/nuxt-vio 22.0.11 → 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.
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>
@@ -4,23 +4,22 @@ export const useGetServiceHref = () => {
4
4
  const isTesting = useIsTesting()
5
5
 
6
6
  return ({
7
- isSsr = true,
7
+ allowInternal = true,
8
8
  name,
9
9
  path,
10
- port,
11
10
  }: {
12
- isSsr?: boolean
13
- name: string
11
+ allowInternal?: boolean
12
+ name: ServiceName
14
13
  path?: string
15
- port?: number
16
14
  }) =>
17
15
  getServiceHref({
16
+ allowInternal,
18
17
  host,
19
- isSsr,
18
+ isServer: import.meta.server,
20
19
  isTesting,
21
20
  name,
22
21
  path,
23
- port,
22
+ services: runtimeConfig.public.vio.services,
24
23
  stagingHost: runtimeConfig.public.vio.stagingHost,
25
24
  })
26
25
  }
@@ -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
+ }
@@ -5,5 +5,4 @@ export const useStrapiFetch = (
5
5
  ...(options ? options : {}),
6
6
  name: options?.name || 'strapi',
7
7
  path: options?.path || '/api',
8
- port: options?.port || 1337,
9
8
  })
@@ -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 {
@@ -0,0 +1,87 @@
1
+ import { SERVICES } from './services'
2
+ import type { Service, ServiceName } from './services'
3
+ import { SITE_URL_TYPED } from './static'
4
+
5
+ export const getHost = (event: { headers: Headers }) => {
6
+ const host = event.headers.get('host')
7
+
8
+ if (!host) throw new Error('Host header is not given!')
9
+
10
+ return host
11
+ }
12
+
13
+ export const getRootHost = (host: string) => {
14
+ const hostParts = host.split('.')
15
+ const hostPartsLast = hostParts[hostParts.length - 1]
16
+
17
+ if (hostParts.length === 1) return hostParts[0]
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
+
27
+ return `${hostParts[hostParts.length - 2]}.${hostPartsLast}`
28
+ }
29
+
30
+ export const getServiceHref = ({
31
+ allowInternal = true,
32
+ host,
33
+ isServer,
34
+ isTesting,
35
+ name,
36
+ path,
37
+ protocol = SITE_URL_TYPED.protocol,
38
+ services = SERVICES,
39
+ stagingHost,
40
+ }: {
41
+ allowInternal?: boolean
42
+ host?: string
43
+ isServer: boolean
44
+ isTesting?: boolean
45
+ name: ServiceName
46
+ path?: string
47
+ protocol?: string
48
+ services?: Record<string, Service>
49
+ stagingHost?: string
50
+ }) => {
51
+ const service = services[name]
52
+
53
+ if (!service) throw new Error(`Service "${name}" is not registered!`)
54
+
55
+ const { hasSubdomain, port } = service
56
+ const nameSubdomainString = `${name.replaceAll('_', '-')}.`
57
+ const pathString = path ? `/${path.replace(/^\/+/, '')}` : ''
58
+
59
+ const assertHasSubdomain = () => {
60
+ if (!hasSubdomain)
61
+ throw new Error(`Service "${name}" has no public subdomain!`)
62
+ }
63
+
64
+ if (isTesting) {
65
+ assertHasSubdomain()
66
+
67
+ return `${protocol}//${nameSubdomainString}${SITE_URL_TYPED.host}${pathString}`
68
+ }
69
+
70
+ if (stagingHost) {
71
+ assertHasSubdomain()
72
+
73
+ return `${protocol}//${nameSubdomainString}${stagingHost}${pathString}`
74
+ }
75
+
76
+ if (isServer && allowInternal) {
77
+ return `http://${name}:${port}${pathString}`
78
+ }
79
+
80
+ if (host) {
81
+ assertHasSubdomain()
82
+
83
+ return `${protocol}//${nameSubdomainString}${getRootHost(host)}${pathString}`
84
+ }
85
+
86
+ throw new Error('Could not get service href!')
87
+ }
@@ -0,0 +1,11 @@
1
+ export type Service = {
2
+ hasSubdomain: boolean
3
+ port: number
4
+ }
5
+
6
+ export const SERVICES = {
7
+ strapi: { hasSubdomain: true, port: 1337 },
8
+ } as const satisfies Record<string, Service>
9
+
10
+ // The open string branch accepts services registered by extending projects through VIO_NUXT_BASE_CONFIG's `services` option, while still keeping autocomplete for the built-in names.
11
+ export type ServiceName = keyof typeof SERVICES | (string & {})
@@ -1,2 +1,3 @@
1
1
  export * from './environment'
2
2
  export * from './nuxt'
3
+ export * from './sentry'
@@ -1,11 +1,15 @@
1
1
  import type { defineNuxtConfig } from 'nuxt/config'
2
2
 
3
+ import type { Service } from '../services'
4
+ import { SERVICES } from '../services'
3
5
  import { IS_IN_FRONTEND_DEVELOPMENT } from './'
4
6
 
5
7
  export const VIO_NUXT_BASE_CONFIG = ({
8
+ services,
6
9
  siteName,
7
10
  stagingHost,
8
11
  }: {
12
+ services?: Record<string, Service>
9
13
  siteName: string
10
14
  stagingHost?: string
11
15
  }) =>
@@ -18,6 +22,7 @@ export const VIO_NUXT_BASE_CONFIG = ({
18
22
  runtimeConfig: {
19
23
  public: {
20
24
  vio: {
25
+ services: { ...SERVICES, ...services },
21
26
  stagingHost: IS_IN_FRONTEND_DEVELOPMENT ? stagingHost : undefined,
22
27
  },
23
28
  },
@@ -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,6 +358,16 @@ 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)
295
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.
@@ -302,6 +380,7 @@ export default defineNuxtConfig(
302
380
  credits: false,
303
381
  zeroRuntime: true,
304
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
305
384
 
306
385
  // environments
307
386
  $development: {
@@ -327,6 +406,9 @@ export default defineNuxtConfig(
327
406
  strictTransportSecurity: false, // prevent endless reload in Chrome
328
407
  },
329
408
  },
409
+ sentry: {
410
+ enabled: false,
411
+ },
330
412
  site: {
331
413
  debug: true,
332
414
  },
package/package.json CHANGED
@@ -12,13 +12,14 @@
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.16",
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.14.0",
22
+ "@takumi-rs/core": "2.15.1",
22
23
  "@types/lodash-es": "4.17.12",
23
24
  "@types/nodemailer": "8.0.2",
24
25
  "@urql/core": "6.0.3",
@@ -54,9 +55,9 @@
54
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.6.0",
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
63
  "prettier": "3.9.9",
@@ -74,7 +75,7 @@
74
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": "22.0.11",
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,7 +1,6 @@
1
- import { defineEventHandler } from 'h3'
1
+ import { defineEventHandler } from 'nuxt/server'
2
2
 
3
3
  export default defineEventHandler((event) => {
4
- const { res } = event.node
5
- res.setHeader('Content-Type', 'text/plain')
6
- res.end('OK')
4
+ event.res.headers.set('Content-Type', 'text/plain')
5
+ return 'OK'
7
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
@@ -6,23 +6,22 @@ export const useGetServiceHref = ({ event }: { event?: H3Event } = {}) => {
6
6
  const isTesting = useIsTesting()
7
7
 
8
8
  return ({
9
- isSsr = true,
9
+ allowInternal = true,
10
10
  name,
11
11
  path,
12
- port,
13
12
  }: {
14
- isSsr?: boolean
15
- name: string
13
+ allowInternal?: boolean
14
+ name: ServiceName
16
15
  path?: string
17
- port?: number
18
16
  }) =>
19
17
  getServiceHref({
18
+ allowInternal,
20
19
  host,
21
- isSsr,
20
+ isServer: import.meta.server,
22
21
  isTesting,
23
22
  name,
24
23
  path,
25
- port,
24
+ services: runtimeConfig.public.vio.services,
26
25
  stagingHost: runtimeConfig.public.vio.stagingHost,
27
26
  })
28
27
  }
@@ -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,11 +1,11 @@
1
1
  import type { CombinedError } from '@urql/core'
2
- import type { H3Event } from 'h3'
3
2
  import { computed, reactive } from 'vue'
4
3
  import type { Ref } from 'vue'
5
4
 
6
- import { SITE_URL_TYPED } from '../../node/static'
7
5
  import type { ApiData, BackendError } from '../types/api'
8
6
 
7
+ export { getHost, getRootHost, getServiceHref } from '../../node/networking'
8
+
9
9
  export const getApiDataDefault = (): ApiData =>
10
10
  computed(() =>
11
11
  reactive({
@@ -64,70 +64,3 @@ export const getCombinedErrorMessages = (
64
64
 
65
65
  return errorMessages
66
66
  }
67
-
68
- export const getRootHost = (host: string) => {
69
- const hostParts = host.split('.')
70
- const hostPartsLast = hostParts[hostParts.length - 1]
71
-
72
- if (hostParts.length === 1) return hostParts[0]
73
-
74
- // only a single subdomain directly on bare localhost collapses to the root; e.g. app.localhost must keep its "app" label
75
- if (
76
- hostParts.length === 2 &&
77
- hostPartsLast &&
78
- /^localhost(:[0-9]+)?$/.test(hostPartsLast)
79
- )
80
- return hostPartsLast
81
-
82
- return `${hostParts[hostParts.length - 2]}.${hostPartsLast}`
83
- }
84
-
85
- export const getHost = (event: H3Event) => {
86
- const host = event.node.req.headers.host
87
-
88
- if (!host) throw new Error('Host header is not given!')
89
-
90
- return host
91
- }
92
-
93
- export const getServiceHref = ({
94
- host,
95
- isSsr = true,
96
- isTesting,
97
- name,
98
- path,
99
- port,
100
- stagingHost,
101
- }: {
102
- host?: string
103
- isSsr?: boolean
104
- isTesting?: boolean
105
- name: string
106
- path?: string
107
- port?: number
108
- stagingHost?: string
109
- }) => {
110
- const nameSubdomain =
111
- name !== VIO_SITE_NAME ? name?.replaceAll('_', '-') : undefined
112
- const nameSubdomainString = nameSubdomain ? `${nameSubdomain}.` : ''
113
- const portString = port ? `:${port}` : ''
114
- const pathString = path ? `/${path.replace(/^\/+/, '')}` : ''
115
-
116
- if (isTesting) {
117
- return `${SITE_URL_TYPED.protocol}//${nameSubdomainString}${SITE_URL_TYPED.host}${pathString}`
118
- }
119
-
120
- if (stagingHost) {
121
- return `https://${nameSubdomainString}${stagingHost}${pathString}`
122
- }
123
-
124
- if (import.meta.server && isSsr) {
125
- return `http://${name}${portString}${pathString}`
126
- }
127
-
128
- if (host) {
129
- return `https://${nameSubdomainString}${getRootHost(host)}${pathString}`
130
- }
131
-
132
- throw new Error('Could not get service href!')
133
- }
@@ -0,0 +1,2 @@
1
+ export { SERVICES } from '../../node/services'
2
+ export type { Service, ServiceName } from '../../node/services'
@@ -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
- })