Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
VITE_APP_NAME="MapRoulette"
VITE_APP_DESCRIPTION="MapRoulette 4 Local Development Environment"
VITE_API_BASE_URL="https://maproulette.org"
VITE_MAP_ROULETTE_SERVER_WEBSOCKET_URL="wss://maproulette.org/ws"
VITE_SERVER_API_KEY=""
# Base URL for short links embedded in OSM changeset comments
# (e.g. https://mpr.lt/c/<challengeId>/t/<taskId>). If unset,
Expand Down
1 change: 0 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ VITE_APP_DESCRIPTION="MapRoulette E2E test environment"
VITE_APP_URL="http://localhost:3005"

VITE_API_BASE_URL="http://localhost:9000"
VITE_MAP_ROULETTE_SERVER_WEBSOCKET_URL="ws://localhost:9000/ws"
3 changes: 2 additions & 1 deletion src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import ky from 'ky'

export const apiKey = window.env.VITE_SERVER_API_KEY
export const apiBaseUrl = window.env.VITE_API_BASE_URL || 'http://127.0.0.1:9000'

export const apiRequest = ky.extend({
prefixUrl: window.env.VITE_API_BASE_URL || 'http://127.0.0.1:9000',
prefixUrl: apiBaseUrl,
credentials: 'include',
timeout: 60000,
retry: {
Expand Down
3 changes: 2 additions & 1 deletion src/contexts/WebSocketContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react'
import { createContext, useCallback, useContext, useEffect, useMemo } from 'react'
import useWebSocketHook, { ReadyState } from 'react-use-websocket'
import { apiBaseUrl } from '@/api/client'
import { wsLogger } from '@/lib/logger'
import type { WebSocketMessageTypes } from '@/types/WebSocket'
import { useAuthContext } from './AuthContext'
Expand All @@ -18,7 +19,7 @@ export const WebSocketProvider = ({ children }: { children: ReactNode }) => {
const { user } = useAuthContext()

const { lastMessage, readyState, sendMessage } = useWebSocketHook(
user ? import.meta.env.VITE_MAP_ROULETTE_SERVER_WEBSOCKET_URL : null,
user ? `${apiBaseUrl.replace(/^http/, 'ws').replace(/\/$/, '')}/ws` : null,
{
shouldReconnect: () => user !== null,
reconnectAttempts: 10,
Expand Down
1 change: 0 additions & 1 deletion src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ interface AppEnv {
readonly VITE_APP_NAME: string
readonly VITE_APP_DESCRIPTION: string
readonly VITE_API_BASE_URL: string | undefined
readonly VITE_MAP_ROULETTE_SERVER_WEBSOCKET_URL: string
readonly VITE_SERVER_API_KEY: string | undefined
readonly VITE_APP_URL: string | undefined
readonly VITE_OSM_SERVER: string | undefined
Expand Down
49 changes: 43 additions & 6 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import tailwindcss from '@tailwindcss/vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
import viteReact from '@vitejs/plugin-react'
import { playwright } from '@vitest/browser-playwright'
import { loadEnv, type Plugin } from 'vite'
import { loadEnv, type Plugin, type ProxyOptions } from 'vite'
import svgr from 'vite-plugin-svgr'
import { defineConfig } from 'vitest/config'

Expand Down Expand Up @@ -73,26 +73,63 @@ const TESTED_TSX_FILES = [
// local .env file. For release builds, env.json is written to dist/ and contains
// defaults (from .env.example) which can be overridden at deploy time (e.g. via
// env vars on the Docker container; see docker/90-write-env-to-json.sh)
//
// In dev mode the backend is proxied through the dev server, and env.json points
// VITE_API_BASE_URL at the dev server itself. This keeps the backend's session
// cookie first-party, which a SameSite=Lax cookie needs when the backend is
// remote (e.g. developing against production).
function runtimeEnv(): Plugin {
let json: string
let env: Record<string, string>
return {
name: 'maproulette:runtime-env',
configResolved(config) {
json = JSON.stringify(loadEnv(config.mode, config.root, 'VITE_'), null, 2)
config(config, { mode, command }) {
env = loadEnv(mode, resolve(config.root ?? process.cwd()), 'VITE_')
if (command !== 'serve') return
const target = env.VITE_API_BASE_URL || 'http://127.0.0.1:9000'
const http: ProxyOptions = {
target,
changeOrigin: true,
cookieDomainRewrite: '',
configure: (proxy) => {
// The backend derives the OAuth redirect_uri from Origin, falling
// back to its configured frontend. Browsers omit Origin on
// same-origin GETs, so supply the dev server's.
proxy.on('proxyReq', (proxyReq, req) => {
if (!req.headers.origin) proxyReq.setHeader('origin', `http://${req.headers.host}`)
})
// The dev server is plain http, where browsers may drop Secure cookies.
proxy.on('proxyRes', (res) => {
res.headers['set-cookie'] = res.headers['set-cookie']?.map((c) =>
c.replace(/;\s*Secure/i, '')
)
})
},
}
return {
server: {
proxy: {
'/api': http,
'/auth': http,
'/ws': { target, changeOrigin: true, ws: true },
},
},
}
},
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url !== '/env.json') return next()
res.setHeader('Content-Type', 'application/json')
res.setHeader('Cache-Control', 'no-store')
res.end(json)
res.end(
JSON.stringify({ ...env, VITE_API_BASE_URL: `http://${req.headers.host}` }, null, 2)
)
})
},
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'env.json',
source: json,
source: JSON.stringify(env, null, 2),
})
},
}
Expand Down
Loading