@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.
- package/.config/certificates/.gitignore +2 -0
- package/.config/certificates/mkcert.sh +0 -0
- package/app/app.config.ts +0 -60
- package/app/components/vio/button/VioButtonHome.vue +1 -1
- package/app/components/vio/layout/VioLayoutBreadcrumbs.vue +1 -1
- package/app/composables/sentry.ts +17 -0
- package/app/stores/auth.ts +13 -0
- package/node/networking.ts +10 -7
- package/node/static/index.ts +1 -0
- package/node/static/sentry.ts +42 -0
- package/nuxt.config.ts +100 -17
- package/package.json +35 -32
- package/sentry.client.config.ts +38 -0
- package/sentry.server.config.ts +36 -0
- package/server/api/healthcheck.get.ts +4 -3
- package/server/middleware/dateTime.ts +3 -0
- package/server/middleware/headers.ts +4 -2
- package/server/middleware/sentry.ts +33 -0
- package/server/plugins/nodemailer.ts +1 -0
- package/server/utils/dateTime.ts +4 -8
- package/server/utils/dependencies/turnstile.ts +2 -0
- package/server/utils/site.ts +5 -0
- package/server/utils/testing.ts +11 -1
- package/shared/types/app-config.d.ts +62 -0
- package/shared/types/request-event-context.d.ts +13 -0
- package/shared/utils/constants.ts +18 -1
- package/server/plugins/seo.ts +0 -7
|
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
|
-
}
|
|
@@ -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
|
+
}
|
package/app/stores/auth.ts
CHANGED
|
@@ -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 {
|
package/node/networking.ts
CHANGED
|
@@ -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:
|
|
8
|
-
const host = event.
|
|
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
|
|
package/node/static/index.ts
CHANGED
|
@@ -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 {
|
|
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({
|
|
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
|
-
|
|
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.
|
|
4
|
-
"@eslint/compat": "2.1.
|
|
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.
|
|
6
|
+
"@http-util/status-i18n": "0.9.1",
|
|
7
7
|
"@intlify/eslint-plugin-vue-i18n": "4.5.1",
|
|
8
|
-
"@lucide/vue": "1.
|
|
9
|
-
"@nuxt/devtools": "3.4.
|
|
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": "
|
|
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.
|
|
22
|
+
"@takumi-rs/core": "2.15.1",
|
|
22
23
|
"@types/lodash-es": "4.17.12",
|
|
23
|
-
"@types/nodemailer": "8.0.
|
|
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": "
|
|
29
|
+
"@vueuse/core": "15.0.0",
|
|
29
30
|
"class-variance-authority": "0.7.1",
|
|
30
31
|
"clsx": "2.1.1",
|
|
31
|
-
"eslint": "10.
|
|
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.
|
|
36
|
-
"globals": "17.
|
|
36
|
+
"eslint-plugin-yml": "3.9.0",
|
|
37
|
+
"globals": "17.13.0",
|
|
37
38
|
"jiti": "2.7.0",
|
|
38
|
-
"jose": "6.2.
|
|
39
|
-
"nodemailer": "
|
|
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.
|
|
43
|
+
"reka-ui": "2.11.0",
|
|
43
44
|
"shadcn-nuxt": "2.8.2",
|
|
44
|
-
"tailwind-merge": "3.
|
|
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.
|
|
49
|
+
"vue-tsc": "3.3.11"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@types/node": "24.
|
|
52
|
+
"@types/node": "24.19.1",
|
|
52
53
|
"@urql/devtools": "2.0.3",
|
|
53
|
-
"@urql/exchange-graphcache": "9.0.
|
|
54
|
-
"@vueuse/core": "
|
|
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.
|
|
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.
|
|
63
|
+
"prettier": "3.9.9",
|
|
63
64
|
"prettier-plugin-tailwindcss": "0.8.1",
|
|
64
65
|
"serve": "14.2.6",
|
|
65
|
-
"sharp": "0.35.
|
|
66
|
-
"stylelint": "17.
|
|
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.
|
|
72
|
+
"stylelint-no-unsupported-browser-features": "8.1.2",
|
|
72
73
|
"tailwindcss": "4.3.3",
|
|
73
|
-
"vue": "3.5.
|
|
74
|
-
"vue-router": "5.
|
|
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.
|
|
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.
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
+
})
|
package/server/utils/dateTime.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type
|
|
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:
|
|
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
|
-
|
|
30
|
-
|
|
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 }>(
|
package/server/utils/site.ts
CHANGED
|
@@ -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),
|
package/server/utils/testing.ts
CHANGED
|
@@ -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 =
|
|
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 = ({
|
|
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': [
|