Merge API remote base URL config into dev

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-06-24 18:19:02 +03:00
9 changed files with 26 additions and 18 deletions
+3 -3
View File
@@ -15,10 +15,10 @@
# is honored — AZ-498 / contract @ # is honored — AZ-498 / contract @
# _docs/02_document/contracts/satellite-provider/tiles.md) # _docs/02_document/contracts/satellite-provider/tiles.md)
# Prefix for every API request (production: empty; tests / alt deployments: set). # Prefix for every API request (production: empty; remote API / tests: set).
# A trailing slash is stripped automatically. # A trailing slash is stripped automatically.
# Example: VITE_API_BASE_URL=http://azaion-ui:80 # Example: VITE_API_BASE_URL=https://api.azaion.com
VITE_API_BASE_URL= VITE_API_BASE_URL=https://api.azaion.com
# OpenWeatherMap API key. Required for the FlightsPage weather feature. # OpenWeatherMap API key. Required for the FlightsPage weather feature.
# Leave unset in CI tests — the e2e profile routes to owm-stub. # Leave unset in CI tests — the e2e profile routes to owm-stub.
+9 -2
View File
@@ -38,6 +38,13 @@ export function getApiBase(): string {
return raw.replace(/\/+$/, '') return raw.replace(/\/+$/, '')
} }
export function authenticatedApiUrl(path: string): string {
const url = getApiBase() + path
if (!accessToken) return url
const separator = url.includes('?') ? '&' : '?'
return `${url}${separator}access_token=${encodeURIComponent(accessToken)}`
}
/** /**
* Indirection for the failed-refresh redirect. Default impl writes * Indirection for the failed-refresh redirect. Default impl writes
* `'/login'` to `window.location.href` — the production behavior. Tests * `'/login'` to `window.location.href` — the production behavior. Tests
@@ -68,13 +75,13 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
if (options.body && typeof options.body === 'string') headers.set('Content-Type', 'application/json') if (options.body && typeof options.body === 'string') headers.set('Content-Type', 'application/json')
const fullUrl = getApiBase() + url const fullUrl = getApiBase() + url
let res = await fetch(fullUrl, { ...options, headers }) let res = await fetch(fullUrl, { ...options, headers, credentials: 'include' })
if (res.status === 401 && accessToken) { if (res.status === 401 && accessToken) {
const refreshed = await refreshToken() const refreshed = await refreshToken()
if (refreshed) { if (refreshed) {
headers.set('Authorization', `Bearer ${accessToken}`) headers.set('Authorization', `Bearer ${accessToken}`)
res = await fetch(fullUrl, { ...options, headers }) res = await fetch(fullUrl, { ...options, headers, credentials: 'include' })
} else { } else {
setToken(null) setToken(null)
navigateToLoginImpl() navigateToLoginImpl()
+1 -1
View File
@@ -1,3 +1,3 @@
export { api, setToken, getToken, getApiBase, setNavigateToLogin } from './client' export { api, setToken, getToken, getApiBase, authenticatedApiUrl, setNavigateToLogin } from './client'
export { createSSE } from './sse' export { createSSE } from './sse'
export { endpoints } from './endpoints' export { endpoints } from './endpoints'
+2 -2
View File
@@ -5,10 +5,10 @@ export function createSSE<T>(
onMessage: (data: T) => void, onMessage: (data: T) => void,
onError?: (err: Event) => void, onError?: (err: Event) => void,
): () => void { ): () => void {
const token = getToken()
const prefixedUrl = getApiBase() + url const prefixedUrl = getApiBase() + url
const token = getToken()
const fullUrl = token const fullUrl = token
? `${prefixedUrl}${prefixedUrl.includes('?') ? '&' : '?'}access_token=${token}` ? `${prefixedUrl}${prefixedUrl.includes('?') ? '&' : '?'}access_token=${encodeURIComponent(token)}`
: prefixedUrl : prefixedUrl
const source = new EventSource(fullUrl) const source = new EventSource(fullUrl)
+2 -2
View File
@@ -1,6 +1,6 @@
import { useState, useCallback, useEffect, useMemo, useRef } from 'react' import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { api, endpoints } from '../../api' import { api, endpoints, authenticatedApiUrl } from '../../api'
import MediaList from './MediaList' import MediaList from './MediaList'
import VideoPlayer, { type VideoPlayerHandle } from './VideoPlayer' import VideoPlayer, { type VideoPlayerHandle } from './VideoPlayer'
import CanvasEditor, { type CanvasEditorHandle } from './CanvasEditor' import CanvasEditor, { type CanvasEditorHandle } from './CanvasEditor'
@@ -195,7 +195,7 @@ export default function AnnotationsPage() {
img.crossOrigin = 'anonymous' img.crossOrigin = 'anonymous'
img.src = selectedMedia.path.startsWith('blob:') img.src = selectedMedia.path.startsWith('blob:')
? selectedMedia.path ? selectedMedia.path
: endpoints.annotations.mediaFile(selectedMedia.id) : authenticatedApiUrl(endpoints.annotations.mediaFile(selectedMedia.id))
await new Promise(res => { img.onload = res; img.onerror = res }) await new Promise(res => { img.onload = res; img.onerror = res })
w = img.naturalWidth w = img.naturalWidth
h = img.naturalHeight h = img.naturalHeight
+3 -3
View File
@@ -1,5 +1,5 @@
import { useRef, useEffect, useState, useCallback, forwardRef, useImperativeHandle } from 'react' import { useRef, useEffect, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
import { endpoints } from '../../api' import { endpoints, authenticatedApiUrl } from '../../api'
import { MediaType } from '../../types' import { MediaType } from '../../types'
import type { Media, AnnotationListItem, Detection, Affiliation, CombatReadiness } from '../../types' import type { Media, AnnotationListItem, Detection, Affiliation, CombatReadiness } from '../../types'
import { getClassColor, getClassNameFallback, hexToRgba } from '../../class-colors' import { getClassColor, getClassNameFallback, hexToRgba } from '../../class-colors'
@@ -112,11 +112,11 @@ const CanvasEditor = forwardRef<CanvasEditorHandle, Props>(function CanvasEditor
img.crossOrigin = 'anonymous' img.crossOrigin = 'anonymous'
const isLocalPath = media.path.startsWith('blob:') || media.path.startsWith('data:') const isLocalPath = media.path.startsWith('blob:') || media.path.startsWith('data:')
if (annotation && !isLocalPath) { if (annotation && !isLocalPath) {
img.src = endpoints.annotations.annotationImage(annotation.id) img.src = authenticatedApiUrl(endpoints.annotations.annotationImage(annotation.id))
} else if (isLocalPath) { } else if (isLocalPath) {
img.src = media.path img.src = media.path
} else { } else {
img.src = endpoints.annotations.mediaFile(media.id) img.src = authenticatedApiUrl(endpoints.annotations.mediaFile(media.id))
} }
img.onload = () => { img.onload = () => {
imgRef.current = img imgRef.current = img
+2 -2
View File
@@ -1,5 +1,5 @@
import { useRef, useState, useCallback, useEffect, forwardRef, useImperativeHandle } from 'react' import { useRef, useState, useCallback, useEffect, forwardRef, useImperativeHandle } from 'react'
import { endpoints } from '../../api' import { endpoints, authenticatedApiUrl } from '../../api'
import type { Media } from '../../types' import type { Media } from '../../types'
interface Props { interface Props {
@@ -49,7 +49,7 @@ const VideoPlayer = forwardRef<VideoPlayerHandle, Props>(function VideoPlayer({
const videoUrl = media.path.startsWith('blob:') const videoUrl = media.path.startsWith('blob:')
? media.path ? media.path
: endpoints.annotations.mediaFile(media.id) : authenticatedApiUrl(endpoints.annotations.mediaFile(media.id))
const stepFrames = useCallback((count: number) => { const stepFrames = useCallback((count: number) => {
const video = videoRef.current const video = videoRef.current
+2 -2
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useMemo } from 'react' import { useState, useEffect, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { api, endpoints } from '../../api' import { api, endpoints, authenticatedApiUrl } from '../../api'
import { useDebounce } from '../../hooks' import { useDebounce } from '../../hooks'
import { useFlight } from '../../components' import { useFlight } from '../../components'
import { useSavedAnnotations } from '../../components/SavedAnnotationsContext' import { useSavedAnnotations } from '../../components/SavedAnnotationsContext'
@@ -97,7 +97,7 @@ export default function DatasetPage() {
imageName: item.imageName, imageName: item.imageName,
status: item.status, status: item.status,
createdDate: item.createdDate, createdDate: item.createdDate,
thumbnailUrl: endpoints.annotations.annotationThumbnail(item.annotationId), thumbnailUrl: authenticatedApiUrl(endpoints.annotations.annotationThumbnail(item.annotationId)),
isSeed: item.isSeed, isSeed: item.isSeed,
isLocal: false, isLocal: false,
})) }))
+2 -1
View File
@@ -13,8 +13,9 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:8080', target: 'https://api.azaion.com',
changeOrigin: true, changeOrigin: true,
secure: true,
}, },
}, },
}, },