@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 +0 -60
- package/app/components/vio/button/VioButtonHome.vue +1 -1
- package/app/components/vio/layout/VioLayoutBreadcrumbs.vue +1 -1
- package/app/composables/networking.ts +6 -7
- package/app/composables/sentry.ts +17 -0
- package/app/composables/strapi.ts +0 -1
- package/app/stores/auth.ts +13 -0
- package/node/networking.ts +87 -0
- package/node/services.ts +11 -0
- package/node/static/index.ts +1 -0
- package/node/static/nuxt.ts +5 -0
- package/node/static/sentry.ts +42 -0
- package/nuxt.config.ts +99 -17
- package/package.json +11 -8
- package/sentry.client.config.ts +38 -0
- package/sentry.server.config.ts +36 -0
- package/server/api/healthcheck.get.ts +3 -4
- 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/networking.ts +6 -7
- 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/shared/utils/networking.ts +2 -69
- package/shared/utils/services.ts +2 -0
- package/server/plugins/seo.ts +0 -7
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
|
-
}
|
|
@@ -4,23 +4,22 @@ export const useGetServiceHref = () => {
|
|
|
4
4
|
const isTesting = useIsTesting()
|
|
5
5
|
|
|
6
6
|
return ({
|
|
7
|
-
|
|
7
|
+
allowInternal = true,
|
|
8
8
|
name,
|
|
9
9
|
path,
|
|
10
|
-
port,
|
|
11
10
|
}: {
|
|
12
|
-
|
|
13
|
-
name:
|
|
11
|
+
allowInternal?: boolean
|
|
12
|
+
name: ServiceName
|
|
14
13
|
path?: string
|
|
15
|
-
port?: number
|
|
16
14
|
}) =>
|
|
17
15
|
getServiceHref({
|
|
16
|
+
allowInternal,
|
|
18
17
|
host,
|
|
19
|
-
|
|
18
|
+
isServer: import.meta.server,
|
|
20
19
|
isTesting,
|
|
21
20
|
name,
|
|
22
21
|
path,
|
|
23
|
-
|
|
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
|
+
}
|
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 {
|
|
@@ -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
|
+
}
|
package/node/services.ts
ADDED
|
@@ -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 & {})
|
package/node/static/index.ts
CHANGED
package/node/static/nuxt.ts
CHANGED
|
@@ -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 {
|
|
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,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": "
|
|
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
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.
|
|
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.
|
|
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": "
|
|
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 '
|
|
1
|
+
import { defineEventHandler } from 'nuxt/server'
|
|
2
2
|
|
|
3
3
|
export default defineEventHandler((event) => {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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 }>(
|
|
@@ -6,23 +6,22 @@ export const useGetServiceHref = ({ event }: { event?: H3Event } = {}) => {
|
|
|
6
6
|
const isTesting = useIsTesting()
|
|
7
7
|
|
|
8
8
|
return ({
|
|
9
|
-
|
|
9
|
+
allowInternal = true,
|
|
10
10
|
name,
|
|
11
11
|
path,
|
|
12
|
-
port,
|
|
13
12
|
}: {
|
|
14
|
-
|
|
15
|
-
name:
|
|
13
|
+
allowInternal?: boolean
|
|
14
|
+
name: ServiceName
|
|
16
15
|
path?: string
|
|
17
|
-
port?: number
|
|
18
16
|
}) =>
|
|
19
17
|
getServiceHref({
|
|
18
|
+
allowInternal,
|
|
20
19
|
host,
|
|
21
|
-
|
|
20
|
+
isServer: import.meta.server,
|
|
22
21
|
isTesting,
|
|
23
22
|
name,
|
|
24
23
|
path,
|
|
25
|
-
|
|
24
|
+
services: runtimeConfig.public.vio.services,
|
|
26
25
|
stagingHost: runtimeConfig.public.vio.stagingHost,
|
|
27
26
|
})
|
|
28
27
|
}
|
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': [
|
|
@@ -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
|
-
}
|