[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:
Oleksandr Bezdieniezhnykh
2026-05-11 00:42:12 +03:00
parent 510df68bcf
commit db181043ca
8 changed files with 98 additions and 9 deletions
+32 -3
View File
@@ -1,13 +1,41 @@
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) {
accessToken = token
}
/** Read-side companion to {@link setToken}. */
export function getToken() {
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> {
if (res.status === 204) return undefined as T
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 (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) {
const refreshed = await refreshToken()
if (refreshed) {
headers.set('Authorization', `Bearer ${accessToken}`)
res = await fetch(url, { ...options, headers })
res = await fetch(fullUrl, { ...options, headers })
} else {
setToken(null)
window.location.href = '/login'
@@ -41,7 +70,7 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
async function refreshToken(): Promise<boolean> {
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
const data = await res.json()
setToken(data.token)
+5 -2
View File
@@ -1,4 +1,4 @@
import { getToken } from './client'
import { getApiBase, getToken } from './client'
export function createSSE<T>(
url: string,
@@ -6,7 +6,10 @@ export function createSSE<T>(
onError?: (err: Event) => void,
): () => void {
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)
+2 -1
View File
@@ -1,4 +1,5 @@
import L from 'leaflet'
import markerIcon from 'leaflet/dist/images/marker-icon.png'
function pinIcon(color: string) {
return L.divIcon({
@@ -15,7 +16,7 @@ export const pointIconBlue = pinIcon('#228be6')
export const pointIconRed = pinIcon('#fa5252')
export const defaultIcon = new L.Icon({
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
iconUrl: markerIcon,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
+8 -2
View File
@@ -52,7 +52,13 @@ export const PURPOSES = [
export const COORDINATE_PRECISION = 8
const trimTrailingSlash = (s: string) => s.replace(/\/+$/, '')
export const TILE_URLS = {
classic: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
satellite: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
classic:
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
+12
View File
@@ -2,3 +2,15 @@
declare module '*.css'
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
}