mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 08:11:10 +00:00
[AZ-450] [AZ-451] [AZ-452] [AZ-454] Externalize URLs + accessors
Refactor batch 1 of 2 for the 01-testability-refactoring epic (AZ-447). Minimal-surgical edits to make the UI's external dependencies overridable for the test profiles in _docs/02_document/tests/environment.md. - AZ-450 (C03): tile URLs externalized to VITE_OSM_TILE_URL and VITE_ESRI_TILE_URL with the production strings as defaults. Fixes the AC-N3 / NFT-RES-03 air-gap regression and lets the e2e profile's tile-stub serve tiles deterministically. - AZ-451 (C04): Leaflet marker icon imported from the pinned leaflet package as a Vite asset; removes the unpkg.com CDN reference (also fixes the leaflet@1.7.1 vs ^1.9.4 mismatch). - AZ-452 (C05): getApiBase() accessor reading VITE_API_BASE_URL. request(), refreshToken(), and createSSE() prepend the prefix. Default '' preserves every call site. - AZ-454 (C07): JSDoc on setToken/getToken documenting the test-override intent so future cleanup doesn't delete them. Also: .env.example documents every VITE_* var; vite-env.d.ts declares ImportMetaEnv; .gitignore now excludes /dist and *.tsbuildinfo (tracked-by-mistake cache file removed). Build verification: `bunx tsc -b --noEmit` passes. No tests in this run (testability-run exemption per refactor SKILL — tests land in autodev Step 6). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
# Azaion UI — Vite build-time environment variables.
|
||||||
|
#
|
||||||
|
# All variables here are read at build time via `import.meta.env.VITE_*` and
|
||||||
|
# inlined by Vite. Copy this file to `.env.local` (gitignored) for local
|
||||||
|
# dev; CI / Docker pass the same variables through the build environment.
|
||||||
|
#
|
||||||
|
# Every variable is OPTIONAL. When unset, the SPA falls back to production-
|
||||||
|
# default behavior:
|
||||||
|
# - VITE_API_BASE_URL : '' (relative paths; SPA and suite share nginx)
|
||||||
|
# - VITE_OWM_API_KEY : undefined → getWeatherData returns null
|
||||||
|
# - VITE_OWM_BASE_URL : https://api.openweathermap.org/data/2.5
|
||||||
|
# - VITE_OSM_TILE_URL : https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||||
|
# - VITE_ESRI_TILE_URL : https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}
|
||||||
|
|
||||||
|
# Prefix for every API request (production: empty; tests / alt deployments: set).
|
||||||
|
# A trailing slash is stripped automatically.
|
||||||
|
# Example: VITE_API_BASE_URL=http://azaion-ui:80
|
||||||
|
VITE_API_BASE_URL=
|
||||||
|
|
||||||
|
# OpenWeatherMap API key. Required for the FlightsPage weather feature.
|
||||||
|
# Leave unset in CI tests — the e2e profile routes to owm-stub.
|
||||||
|
VITE_OWM_API_KEY=<your-openweathermap-api-key>
|
||||||
|
|
||||||
|
# OpenWeatherMap REST base URL. Default targets the public endpoint; tests
|
||||||
|
# override to point at the owm-stub service.
|
||||||
|
# Example for the e2e profile: http://owm-stub:8081/data/2.5
|
||||||
|
VITE_OWM_BASE_URL=
|
||||||
|
|
||||||
|
# OSM map tile URL template (Leaflet TileLayer.url).
|
||||||
|
# Example for the e2e profile: http://tile-stub:8082/{z}/{x}/{y}.png
|
||||||
|
VITE_OSM_TILE_URL=
|
||||||
|
|
||||||
|
# Esri satellite tile URL template (Leaflet TileLayer.url for the satellite layer).
|
||||||
|
# Example for the e2e profile: http://tile-stub:8082/sat/{z}/{y}/{x}
|
||||||
|
VITE_ESRI_TILE_URL=
|
||||||
@@ -10,6 +10,10 @@ node_modules/
|
|||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# TypeScript build cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+32
-3
@@ -1,13 +1,41 @@
|
|||||||
let accessToken: string | null = null
|
let accessToken: string | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for the in-memory bearer token.
|
||||||
|
*
|
||||||
|
* The bearer is intentionally held in module scope (NOT in localStorage /
|
||||||
|
* sessionStorage / cookies) per AC-02 / restriction O2 — the only way to
|
||||||
|
* survive a page reload is through the HttpOnly refresh cookie + the
|
||||||
|
* `refreshToken()` round-trip in `request()` below.
|
||||||
|
*
|
||||||
|
* Tests override this hook to seed a bearer without going through the full
|
||||||
|
* login flow. See `_docs/02_document/tests/test-data.md` ("Stubbed bearer /
|
||||||
|
* cookie in test helpers"). DO NOT delete this accessor as "dead code" —
|
||||||
|
* it is reflectively used by the test harness; the import looks dead at
|
||||||
|
* static-grep time but is not.
|
||||||
|
*/
|
||||||
export function setToken(token: string | null) {
|
export function setToken(token: string | null) {
|
||||||
accessToken = token
|
accessToken = token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Read-side companion to {@link setToken}. */
|
||||||
export function getToken() {
|
export function getToken() {
|
||||||
return accessToken
|
return accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the base URL prefix for every API request. Returns `''` (no
|
||||||
|
* prefix) on production builds where the SPA and the suite share the same
|
||||||
|
* nginx (E2); tests + alternative deployments override via the Vite env
|
||||||
|
* var `VITE_API_BASE_URL`. A trailing slash is stripped so a value of
|
||||||
|
* `http://host/` does not produce `http://host//api/...`.
|
||||||
|
*/
|
||||||
|
export function getApiBase(): string {
|
||||||
|
const raw = import.meta.env.VITE_API_BASE_URL
|
||||||
|
if (!raw) return ''
|
||||||
|
return raw.replace(/\/+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
async function handleResponse<T>(res: Response): Promise<T> {
|
async function handleResponse<T>(res: Response): Promise<T> {
|
||||||
if (res.status === 204) return undefined as T
|
if (res.status === 204) return undefined as T
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -22,13 +50,14 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
|||||||
if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`)
|
if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`)
|
||||||
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')
|
||||||
|
|
||||||
let res = await fetch(url, { ...options, headers })
|
const fullUrl = getApiBase() + url
|
||||||
|
let res = await fetch(fullUrl, { ...options, headers })
|
||||||
|
|
||||||
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(url, { ...options, headers })
|
res = await fetch(fullUrl, { ...options, headers })
|
||||||
} else {
|
} else {
|
||||||
setToken(null)
|
setToken(null)
|
||||||
window.location.href = '/login'
|
window.location.href = '/login'
|
||||||
@@ -41,7 +70,7 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
|||||||
|
|
||||||
async function refreshToken(): Promise<boolean> {
|
async function refreshToken(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/auth/refresh', { method: 'POST', credentials: 'include' })
|
const res = await fetch(getApiBase() + '/api/admin/auth/refresh', { method: 'POST', credentials: 'include' })
|
||||||
if (!res.ok) return false
|
if (!res.ok) return false
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setToken(data.token)
|
setToken(data.token)
|
||||||
|
|||||||
+5
-2
@@ -1,4 +1,4 @@
|
|||||||
import { getToken } from './client'
|
import { getApiBase, getToken } from './client'
|
||||||
|
|
||||||
export function createSSE<T>(
|
export function createSSE<T>(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -6,7 +6,10 @@ export function createSSE<T>(
|
|||||||
onError?: (err: Event) => void,
|
onError?: (err: Event) => void,
|
||||||
): () => void {
|
): () => void {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
const fullUrl = token ? `${url}${url.includes('?') ? '&' : '?'}access_token=${token}` : url
|
const prefixedUrl = getApiBase() + url
|
||||||
|
const fullUrl = token
|
||||||
|
? `${prefixedUrl}${prefixedUrl.includes('?') ? '&' : '?'}access_token=${token}`
|
||||||
|
: prefixedUrl
|
||||||
|
|
||||||
const source = new EventSource(fullUrl)
|
const source = new EventSource(fullUrl)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import L from 'leaflet'
|
import L from 'leaflet'
|
||||||
|
import markerIcon from 'leaflet/dist/images/marker-icon.png'
|
||||||
|
|
||||||
function pinIcon(color: string) {
|
function pinIcon(color: string) {
|
||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
@@ -15,7 +16,7 @@ export const pointIconBlue = pinIcon('#228be6')
|
|||||||
export const pointIconRed = pinIcon('#fa5252')
|
export const pointIconRed = pinIcon('#fa5252')
|
||||||
|
|
||||||
export const defaultIcon = new L.Icon({
|
export const defaultIcon = new L.Icon({
|
||||||
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
|
iconUrl: markerIcon,
|
||||||
iconSize: [25, 41],
|
iconSize: [25, 41],
|
||||||
iconAnchor: [12, 41],
|
iconAnchor: [12, 41],
|
||||||
popupAnchor: [1, -34],
|
popupAnchor: [1, -34],
|
||||||
|
|||||||
@@ -52,7 +52,13 @@ export const PURPOSES = [
|
|||||||
|
|
||||||
export const COORDINATE_PRECISION = 8
|
export const COORDINATE_PRECISION = 8
|
||||||
|
|
||||||
|
const trimTrailingSlash = (s: string) => s.replace(/\/+$/, '')
|
||||||
|
|
||||||
export const TILE_URLS = {
|
export const TILE_URLS = {
|
||||||
classic: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
classic:
|
||||||
satellite: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
trimTrailingSlash(import.meta.env.VITE_OSM_TILE_URL ?? '') ||
|
||||||
|
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
satellite:
|
||||||
|
trimTrailingSlash(import.meta.env.VITE_ESRI_TILE_URL ?? '') ||
|
||||||
|
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
Vendored
+12
@@ -2,3 +2,15 @@
|
|||||||
|
|
||||||
declare module '*.css'
|
declare module '*.css'
|
||||||
declare module 'leaflet-polylinedecorator'
|
declare module 'leaflet-polylinedecorator'
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_BASE_URL?: string
|
||||||
|
readonly VITE_OWM_API_KEY?: string
|
||||||
|
readonly VITE_OWM_BASE_URL?: string
|
||||||
|
readonly VITE_OSM_TILE_URL?: string
|
||||||
|
readonly VITE_ESRI_TILE_URL?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/api/sse.ts","./src/auth/authcontext.tsx","./src/auth/protectedroute.tsx","./src/components/confirmdialog.tsx","./src/components/detectionclasses.tsx","./src/components/flightcontext.tsx","./src/components/header.tsx","./src/components/helpmodal.tsx","./src/features/admin/adminpage.tsx","./src/features/annotations/annotationspage.tsx","./src/features/annotations/annotationssidebar.tsx","./src/features/annotations/canvaseditor.tsx","./src/features/annotations/medialist.tsx","./src/features/annotations/videoplayer.tsx","./src/features/dataset/datasetpage.tsx","./src/features/flights/altitudechart.tsx","./src/features/flights/altitudedialog.tsx","./src/features/flights/drawcontrol.tsx","./src/features/flights/flightlistsidebar.tsx","./src/features/flights/flightmap.tsx","./src/features/flights/flightparamspanel.tsx","./src/features/flights/flightspage.tsx","./src/features/flights/jsoneditordialog.tsx","./src/features/flights/mappoint.tsx","./src/features/flights/minimap.tsx","./src/features/flights/waypointlist.tsx","./src/features/flights/windeffect.tsx","./src/features/flights/flightplanutils.ts","./src/features/flights/mapicons.ts","./src/features/flights/types.ts","./src/features/login/loginpage.tsx","./src/features/settings/settingspage.tsx","./src/hooks/usedebounce.ts","./src/hooks/useresizablepanel.ts","./src/i18n/i18n.ts","./src/types/index.ts"],"version":"5.7.3"}
|
|
||||||
Reference in New Issue
Block a user