webapp: harden mapbox/chatwoot runtime config
Some checks failed
Build Docker Image / build (push) Failing after 13m29s

This commit is contained in:
Ruslan Bakiev
2026-03-11 19:01:52 +07:00
parent 29c34a048a
commit 4be7cade98
5 changed files with 82 additions and 27 deletions

View File

@@ -21,16 +21,21 @@
<label class="label">
<span class="label-text">{{ t('profileAddresses.form.address.label') }}</span>
</label>
<div ref="searchBoxContainer" class="w-full" />
<input
v-if="addressData.address"
type="hidden"
:value="addressData.address"
<div
v-if="hasMapboxToken"
ref="searchBoxContainer"
class="w-full"
/>
<Input
v-else
v-model="addressData.address"
:placeholder="t('profileAddresses.form.address.placeholder')"
/>
<input v-if="addressData.address && hasMapboxToken" type="hidden" :value="addressData.address" />
</div>
<!-- Mapbox map for selecting coordinates -->
<div class="form-control">
<div v-if="hasMapboxToken" class="form-control">
<label class="label">
<span class="label-text">{{ t('profileAddresses.form.mapLabel') }}</span>
</label>
@@ -61,9 +66,12 @@
</span>
</label>
</div>
<div v-else class="alert alert-warning text-sm">
Mapbox is not configured. Enter address manually.
</div>
<Stack direction="row" gap="3">
<Button @click="updateAddress" :disabled="isSaving || !addressData.latitude">
<Button @click="updateAddress" :disabled="isSaving || (hasMapboxToken && !addressData.latitude)">
{{ isSaving ? t('profileAddresses.form.updating') : t('profileAddresses.form.update') }}
</Button>
<Button variant="outline" :as="NuxtLink" :to="localePath('/clientarea/addresses')">
@@ -108,6 +116,8 @@ const { execute, mutate } = useGraphQL()
const { t } = useI18n()
const localePath = useLocalePath()
const config = useRuntimeConfig()
const mapboxAccessToken = computed(() => String(config.public.mapboxAccessToken || '').trim())
const hasMapboxToken = computed(() => mapboxAccessToken.value.length > 0)
const uuid = computed(() => route.params.uuid as string)
@@ -161,10 +171,13 @@ const onMapCreated = (map: MapboxMapType) => {
// Reverse geocode: get address by coordinates (local language)
const reverseGeocode = async (lat: number, lng: number): Promise<{ address: string | null; countryCode: string | null }> => {
if (!hasMapboxToken.value) {
return { address: null, countryCode: null }
}
try {
const token = config.public.mapboxAccessToken
const response = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${lng},${lat}.json?access_token=${token}`
`https://api.mapbox.com/geocoding/v5/mapbox.places/${lng},${lat}.json?access_token=${mapboxAccessToken.value}`
)
const data = await response.json()
const feature = data.features?.[0]
@@ -203,12 +216,12 @@ const onMapClick = async (event: MapMouseEvent) => {
// Initialize Mapbox SearchBox
onMounted(async () => {
if (!searchBoxContainer.value) return
if (!hasMapboxToken.value || !searchBoxContainer.value) return
const { MapboxSearchBox } = await import('@mapbox/search-js-web')
const searchBox = new MapboxSearchBox()
searchBox.accessToken = config.public.mapboxAccessToken as string
searchBox.accessToken = mapboxAccessToken.value
searchBox.options = {
// Without language: uses local country language
}
@@ -248,7 +261,7 @@ onMounted(async () => {
})
const updateAddress = async () => {
if (!addressData.value || !addressData.value.name || !addressData.value.address || !addressData.value.latitude) return
if (!addressData.value || !addressData.value.name || !addressData.value.address || (hasMapboxToken.value && !addressData.value.latitude)) return
isSaving.value = true
try {

View File

@@ -14,16 +14,21 @@
<label class="label">
<span class="label-text">{{ t('profileAddresses.form.address.label') }}</span>
</label>
<div ref="searchBoxContainer" class="w-full" />
<input
v-if="newAddress.address"
type="hidden"
:value="newAddress.address"
<div
v-if="hasMapboxToken"
ref="searchBoxContainer"
class="w-full"
/>
<Input
v-else
v-model="newAddress.address"
:placeholder="t('profileAddresses.form.address.placeholder')"
/>
<input v-if="newAddress.address && hasMapboxToken" type="hidden" :value="newAddress.address" />
</div>
<!-- Mapbox map for selecting coordinates -->
<div class="form-control">
<div v-if="hasMapboxToken" class="form-control">
<label class="label">
<span class="label-text">{{ t('profileAddresses.form.mapLabel') }}</span>
</label>
@@ -54,9 +59,12 @@
</span>
</label>
</div>
<div v-else class="alert alert-warning text-sm">
Mapbox is not configured. Enter address manually.
</div>
<Stack direction="row" gap="3">
<Button @click="createAddress" :disabled="isCreating || !newAddress.latitude">
<Button @click="createAddress" :disabled="isCreating || (hasMapboxToken && !newAddress.latitude)">
{{ isCreating ? t('profileAddresses.form.saving') : t('profileAddresses.form.save') }}
</Button>
<Button variant="outline" :as="NuxtLink" :to="localePath('/clientarea/addresses')">
@@ -84,6 +92,8 @@ const { mutate } = useGraphQL()
const { t } = useI18n()
const localePath = useLocalePath()
const config = useRuntimeConfig()
const mapboxAccessToken = computed(() => String(config.public.mapboxAccessToken || '').trim())
const hasMapboxToken = computed(() => mapboxAccessToken.value.length > 0)
const isCreating = ref(false)
const searchBoxContainer = ref<HTMLElement | null>(null)
@@ -104,10 +114,13 @@ const onMapCreated = (map: MapboxMapType) => {
// Reverse geocode: get address by coordinates (local language)
const reverseGeocode = async (lat: number, lng: number): Promise<{ address: string | null; countryCode: string | null }> => {
if (!hasMapboxToken.value) {
return { address: null, countryCode: null }
}
try {
const token = config.public.mapboxAccessToken
const response = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${lng},${lat}.json?access_token=${token}`
`https://api.mapbox.com/geocoding/v5/mapbox.places/${lng},${lat}.json?access_token=${mapboxAccessToken.value}`
)
const data = await response.json()
const feature = data.features?.[0]
@@ -144,12 +157,12 @@ const onMapClick = async (event: MapMouseEvent) => {
// Initialize Mapbox SearchBox
onMounted(async () => {
if (!searchBoxContainer.value) return
if (!hasMapboxToken.value || !searchBoxContainer.value) return
const { MapboxSearchBox } = await import('@mapbox/search-js-web')
const searchBox = new MapboxSearchBox()
searchBox.accessToken = config.public.mapboxAccessToken as string
searchBox.accessToken = mapboxAccessToken.value
searchBox.options = {
// Without language: uses local country language
}
@@ -182,7 +195,7 @@ onMounted(async () => {
})
const createAddress = async () => {
if (!newAddress.name || !newAddress.address || !newAddress.latitude) return
if (!newAddress.name || !newAddress.address || (hasMapboxToken.value && !newAddress.latitude)) return
isCreating.value = true
try {

View File

@@ -0,0 +1,20 @@
export default defineNuxtPlugin(() => {
const originalConsoleError = console.error
console.error = (...args: unknown[]) => {
const hasApolloDevtoolsWarning = args.some((arg) => {
if (typeof arg !== 'string') return false
return (
arg.includes('connectToDevTools') &&
arg.includes('devtools.enabled')
)
})
if (hasApolloDevtoolsWarning) {
return
}
originalConsoleError(...args)
}
})

View File

@@ -1,8 +1,15 @@
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const baseUrl = String(config.public.chatwootBaseUrl || '').trim()
const websiteToken = String(config.public.chatwootWebsiteToken || '').trim()
if (!baseUrl || !websiteToken) {
return
}
const loadChatwoot = () => {
if (document.getElementById('chatwoot-sdk')) return
const baseUrl = 'https://chatwoot.optovia.ru'
const script = document.createElement('script')
script.id = 'chatwoot-sdk'
script.src = `${baseUrl}/packs/js/sdk.js`
@@ -10,7 +17,7 @@ export default defineNuxtPlugin(() => {
script.defer = true
script.onload = () => {
window.chatwootSDK?.run({
websiteToken: 'bc668ge3hM5ZpPeUgGEV1ZU9',
websiteToken,
baseUrl
})
}

View File

@@ -200,7 +200,9 @@ export default defineNuxtConfig({
novuAppId: process.env.NUXT_PUBLIC_NOVU_APP_ID,
novuBackendUrl: process.env.NUXT_PUBLIC_NOVU_BACKEND_URL,
novuSocketUrl: process.env.NUXT_PUBLIC_NOVU_SOCKET_URL,
mapboxAccessToken: process.env.NUXT_PUBLIC_MAPBOX_ACCESS_TOKEN || ''
mapboxAccessToken: process.env.NUXT_PUBLIC_MAPBOX_ACCESS_TOKEN || '',
chatwootBaseUrl: process.env.NUXT_PUBLIC_CHATWOOT_BASE_URL || '',
chatwootWebsiteToken: process.env.NUXT_PUBLIC_CHATWOOT_WEBSITE_TOKEN || ''
}
},
mapbox: {