diff --git a/.env.example b/.env.example index e6bc0ea..5a7da36 100644 --- a/.env.example +++ b/.env.example @@ -15,10 +15,10 @@ # is honored — AZ-498 / contract @ # _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. -# Example: VITE_API_BASE_URL=http://azaion-ui:80 -VITE_API_BASE_URL= +# Example: VITE_API_BASE_URL=https://api.azaion.com +VITE_API_BASE_URL=https://api.azaion.com # OpenWeatherMap API key. Required for the FlightsPage weather feature. # Leave unset in CI tests — the e2e profile routes to owm-stub. diff --git a/src/api/client.ts b/src/api/client.ts index 3a9d48a..29b733f 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -38,6 +38,13 @@ export function getApiBase(): string { 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 * `'/login'` to `window.location.href` — the production behavior. Tests @@ -68,13 +75,13 @@ async function request(url: string, options: RequestInit = {}): Promise { if (options.body && typeof options.body === 'string') headers.set('Content-Type', 'application/json') 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) { const refreshed = await refreshToken() if (refreshed) { headers.set('Authorization', `Bearer ${accessToken}`) - res = await fetch(fullUrl, { ...options, headers }) + res = await fetch(fullUrl, { ...options, headers, credentials: 'include' }) } else { setToken(null) navigateToLoginImpl() diff --git a/src/api/index.ts b/src/api/index.ts index 0b660a9..48c8bd8 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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 { endpoints } from './endpoints' diff --git a/src/api/sse.ts b/src/api/sse.ts index 6a2ac85..3805955 100644 --- a/src/api/sse.ts +++ b/src/api/sse.ts @@ -5,10 +5,10 @@ export function createSSE( onMessage: (data: T) => void, onError?: (err: Event) => void, ): () => void { - const token = getToken() const prefixedUrl = getApiBase() + url + const token = getToken() const fullUrl = token - ? `${prefixedUrl}${prefixedUrl.includes('?') ? '&' : '?'}access_token=${token}` + ? `${prefixedUrl}${prefixedUrl.includes('?') ? '&' : '?'}access_token=${encodeURIComponent(token)}` : prefixedUrl const source = new EventSource(fullUrl) diff --git a/src/features/annotations/AnnotationsPage.tsx b/src/features/annotations/AnnotationsPage.tsx index e50f800..b144dfb 100644 --- a/src/features/annotations/AnnotationsPage.tsx +++ b/src/features/annotations/AnnotationsPage.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { api, endpoints } from '../../api' +import { api, endpoints, authenticatedApiUrl } from '../../api' import MediaList from './MediaList' import VideoPlayer, { type VideoPlayerHandle } from './VideoPlayer' import CanvasEditor, { type CanvasEditorHandle } from './CanvasEditor' @@ -195,7 +195,7 @@ export default function AnnotationsPage() { img.crossOrigin = 'anonymous' img.src = selectedMedia.path.startsWith('blob:') ? selectedMedia.path - : endpoints.annotations.mediaFile(selectedMedia.id) + : authenticatedApiUrl(endpoints.annotations.mediaFile(selectedMedia.id)) await new Promise(res => { img.onload = res; img.onerror = res }) w = img.naturalWidth h = img.naturalHeight diff --git a/src/features/annotations/CanvasEditor.tsx b/src/features/annotations/CanvasEditor.tsx index f69e64b..ac7d548 100644 --- a/src/features/annotations/CanvasEditor.tsx +++ b/src/features/annotations/CanvasEditor.tsx @@ -1,5 +1,5 @@ import { useRef, useEffect, useState, useCallback, forwardRef, useImperativeHandle } from 'react' -import { endpoints } from '../../api' +import { endpoints, authenticatedApiUrl } from '../../api' import { MediaType } from '../../types' import type { Media, AnnotationListItem, Detection, Affiliation, CombatReadiness } from '../../types' import { getClassColor, getClassNameFallback, hexToRgba } from '../../class-colors' @@ -112,11 +112,11 @@ const CanvasEditor = forwardRef(function CanvasEditor img.crossOrigin = 'anonymous' const isLocalPath = media.path.startsWith('blob:') || media.path.startsWith('data:') if (annotation && !isLocalPath) { - img.src = endpoints.annotations.annotationImage(annotation.id) + img.src = authenticatedApiUrl(endpoints.annotations.annotationImage(annotation.id)) } else if (isLocalPath) { img.src = media.path } else { - img.src = endpoints.annotations.mediaFile(media.id) + img.src = authenticatedApiUrl(endpoints.annotations.mediaFile(media.id)) } img.onload = () => { imgRef.current = img diff --git a/src/features/annotations/VideoPlayer.tsx b/src/features/annotations/VideoPlayer.tsx index 46a4b57..8586578 100644 --- a/src/features/annotations/VideoPlayer.tsx +++ b/src/features/annotations/VideoPlayer.tsx @@ -1,5 +1,5 @@ import { useRef, useState, useCallback, useEffect, forwardRef, useImperativeHandle } from 'react' -import { endpoints } from '../../api' +import { endpoints, authenticatedApiUrl } from '../../api' import type { Media } from '../../types' interface Props { @@ -49,7 +49,7 @@ const VideoPlayer = forwardRef(function VideoPlayer({ const videoUrl = media.path.startsWith('blob:') ? media.path - : endpoints.annotations.mediaFile(media.id) + : authenticatedApiUrl(endpoints.annotations.mediaFile(media.id)) const stepFrames = useCallback((count: number) => { const video = videoRef.current diff --git a/src/features/dataset/DatasetPage.tsx b/src/features/dataset/DatasetPage.tsx index 7b79961..c20e004 100644 --- a/src/features/dataset/DatasetPage.tsx +++ b/src/features/dataset/DatasetPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { api, endpoints } from '../../api' +import { api, endpoints, authenticatedApiUrl } from '../../api' import { useDebounce } from '../../hooks' import { useFlight } from '../../components' import { useSavedAnnotations } from '../../components/SavedAnnotationsContext' @@ -97,7 +97,7 @@ export default function DatasetPage() { imageName: item.imageName, status: item.status, createdDate: item.createdDate, - thumbnailUrl: endpoints.annotations.annotationThumbnail(item.annotationId), + thumbnailUrl: authenticatedApiUrl(endpoints.annotations.annotationThumbnail(item.annotationId)), isSeed: item.isSeed, isLocal: false, })) diff --git a/vite.config.ts b/vite.config.ts index 85c9bb6..a92f867 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,8 +13,9 @@ export default defineConfig({ server: { proxy: { '/api': { - target: 'http://localhost:8080', + target: 'https://api.azaion.com', changeOrigin: true, + secure: true, }, }, },