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

View File

@@ -14,16 +14,21 @@
<label class="label"> <label class="label">
<span class="label-text">{{ t('profileAddresses.form.address.label') }}</span> <span class="label-text">{{ t('profileAddresses.form.address.label') }}</span>
</label> </label>
<div ref="searchBoxContainer" class="w-full" /> <div
<input v-if="hasMapboxToken"
v-if="newAddress.address" ref="searchBoxContainer"
type="hidden" class="w-full"
:value="newAddress.address"
/> />
<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> </div>
<!-- Mapbox map for selecting coordinates --> <!-- Mapbox map for selecting coordinates -->
<div class="form-control"> <div v-if="hasMapboxToken" class="form-control">
<label class="label"> <label class="label">
<span class="label-text">{{ t('profileAddresses.form.mapLabel') }}</span> <span class="label-text">{{ t('profileAddresses.form.mapLabel') }}</span>
</label> </label>
@@ -54,9 +59,12 @@
</span> </span>
</label> </label>
</div> </div>
<div v-else class="alert alert-warning text-sm">
Mapbox is not configured. Enter address manually.
</div>
<Stack direction="row" gap="3"> <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') }} {{ isCreating ? t('profileAddresses.form.saving') : t('profileAddresses.form.save') }}
</Button> </Button>
<Button variant="outline" :as="NuxtLink" :to="localePath('/clientarea/addresses')"> <Button variant="outline" :as="NuxtLink" :to="localePath('/clientarea/addresses')">
@@ -84,6 +92,8 @@ const { mutate } = useGraphQL()
const { t } = useI18n() const { t } = useI18n()
const localePath = useLocalePath() const localePath = useLocalePath()
const config = useRuntimeConfig() const config = useRuntimeConfig()
const mapboxAccessToken = computed(() => String(config.public.mapboxAccessToken || '').trim())
const hasMapboxToken = computed(() => mapboxAccessToken.value.length > 0)
const isCreating = ref(false) const isCreating = ref(false)
const searchBoxContainer = ref<HTMLElement | null>(null) const searchBoxContainer = ref<HTMLElement | null>(null)
@@ -104,10 +114,13 @@ const onMapCreated = (map: MapboxMapType) => {
// Reverse geocode: get address by coordinates (local language) // Reverse geocode: get address by coordinates (local language)
const reverseGeocode = async (lat: number, lng: number): Promise<{ address: string | null; countryCode: string | null }> => { const reverseGeocode = async (lat: number, lng: number): Promise<{ address: string | null; countryCode: string | null }> => {
if (!hasMapboxToken.value) {
return { address: null, countryCode: null }
}
try { try {
const token = config.public.mapboxAccessToken
const response = await fetch( 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 data = await response.json()
const feature = data.features?.[0] const feature = data.features?.[0]
@@ -144,12 +157,12 @@ const onMapClick = async (event: MapMouseEvent) => {
// Initialize Mapbox SearchBox // Initialize Mapbox SearchBox
onMounted(async () => { onMounted(async () => {
if (!searchBoxContainer.value) return if (!hasMapboxToken.value || !searchBoxContainer.value) return
const { MapboxSearchBox } = await import('@mapbox/search-js-web') const { MapboxSearchBox } = await import('@mapbox/search-js-web')
const searchBox = new MapboxSearchBox() const searchBox = new MapboxSearchBox()
searchBox.accessToken = config.public.mapboxAccessToken as string searchBox.accessToken = mapboxAccessToken.value
searchBox.options = { searchBox.options = {
// Without language: uses local country language // Without language: uses local country language
} }
@@ -182,7 +195,7 @@ onMounted(async () => {
}) })
const createAddress = 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 isCreating.value = true
try { 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(() => { 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 = () => { const loadChatwoot = () => {
if (document.getElementById('chatwoot-sdk')) return if (document.getElementById('chatwoot-sdk')) return
const baseUrl = 'https://chatwoot.optovia.ru'
const script = document.createElement('script') const script = document.createElement('script')
script.id = 'chatwoot-sdk' script.id = 'chatwoot-sdk'
script.src = `${baseUrl}/packs/js/sdk.js` script.src = `${baseUrl}/packs/js/sdk.js`
@@ -10,7 +17,7 @@ export default defineNuxtPlugin(() => {
script.defer = true script.defer = true
script.onload = () => { script.onload = () => {
window.chatwootSDK?.run({ window.chatwootSDK?.run({
websiteToken: 'bc668ge3hM5ZpPeUgGEV1ZU9', websiteToken,
baseUrl baseUrl
}) })
} }

View File

@@ -200,7 +200,9 @@ export default defineNuxtConfig({
novuAppId: process.env.NUXT_PUBLIC_NOVU_APP_ID, novuAppId: process.env.NUXT_PUBLIC_NOVU_APP_ID,
novuBackendUrl: process.env.NUXT_PUBLIC_NOVU_BACKEND_URL, novuBackendUrl: process.env.NUXT_PUBLIC_NOVU_BACKEND_URL,
novuSocketUrl: process.env.NUXT_PUBLIC_NOVU_SOCKET_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: { mapbox: {