diff --git a/apps/visualizer/vite.config.ts b/apps/visualizer/vite.config.ts index 8c5fe11..6cbf362 100644 --- a/apps/visualizer/vite.config.ts +++ b/apps/visualizer/vite.config.ts @@ -9,6 +9,11 @@ const ROOT = path.resolve(DIR, '../..'); type BasicAuth = { username: string; password: string }; +function stripTrailingSlashes(p: string): string { + const out = p.replace(/\/+$/, ''); + return out || '/'; +} + function readApiReadToken(): string | undefined { if (process.env.API_READ_TOKEN) return process.env.API_READ_TOKEN; const p = path.join(ROOT, 'tokens', 'read.json'); @@ -56,30 +61,98 @@ function readProxyBasicAuth(): BasicAuth | undefined { const apiReadToken = readApiReadToken(); const proxyBasicAuth = readProxyBasicAuth(); +const apiProxyTarget = process.env.API_PROXY_TARGET || 'http://localhost:8787'; + +function parseUrl(v: string): URL | undefined { + try { + return new URL(v); + } catch { + return undefined; + } +} + +const apiProxyTargetUrl = parseUrl(apiProxyTarget); +const apiProxyTargetPath = stripTrailingSlashes(apiProxyTargetUrl?.pathname || '/'); +const apiProxyTargetEndsWithApi = apiProxyTargetPath.endsWith('/api'); + +function inferUiProxyTarget(apiTarget: string): string | undefined { + try { + const u = new URL(apiTarget); + const p = stripTrailingSlashes(u.pathname || '/'); + if (!p.endsWith('/api')) return undefined; + const basePath = p.slice(0, -'/api'.length) || '/'; + u.pathname = basePath; + u.search = ''; + u.hash = ''; + const out = u.toString(); + return out.endsWith('/') ? out.slice(0, -1) : out; + } catch { + return undefined; + } +} + +const uiProxyTarget = + process.env.FRONTEND_PROXY_TARGET || + process.env.UI_PROXY_TARGET || + process.env.AUTH_PROXY_TARGET || + inferUiProxyTarget(apiProxyTarget) || + (apiProxyTargetUrl && apiProxyTargetPath === '/' ? stripTrailingSlashes(apiProxyTargetUrl.toString()) : undefined); + +function applyProxyBasicAuth(proxyReq: any) { + if (!proxyBasicAuth) return false; + const b64 = Buffer.from(`${proxyBasicAuth.username}:${proxyBasicAuth.password}`, 'utf8').toString('base64'); + proxyReq.setHeader('Authorization', `Basic ${b64}`); + return true; +} + +function rewriteSetCookieForLocalDevHttp(proxyRes: any) { + const v = proxyRes?.headers?.['set-cookie']; + if (!v) return; + const rewrite = (cookie: string) => { + let out = cookie.replace(/;\s*secure\b/gi, ''); + out = out.replace(/;\s*domain=[^;]+/gi, ''); + out = out.replace(/;\s*samesite=none\b/gi, '; SameSite=Lax'); + return out; + }; + proxyRes.headers['set-cookie'] = Array.isArray(v) ? v.map(rewrite) : rewrite(String(v)); +} + +const proxy: Record = { + '/api': { + target: apiProxyTarget, + changeOrigin: true, + rewrite: (p: string) => (apiProxyTargetEndsWithApi ? p.replace(/^\/api/, '') : p), + configure: (p: any) => { + p.on('proxyReq', (proxyReq: any) => { + if (applyProxyBasicAuth(proxyReq)) return; + if (apiReadToken) proxyReq.setHeader('Authorization', `Bearer ${apiReadToken}`); + }); + }, + }, +}; + +if (uiProxyTarget) { + for (const prefix of ['/whoami', '/auth', '/logout']) { + proxy[prefix] = { + target: uiProxyTarget, + changeOrigin: true, + configure: (p: any) => { + p.on('proxyReq', (proxyReq: any) => { + applyProxyBasicAuth(proxyReq); + }); + p.on('proxyRes', (proxyRes: any) => { + rewriteSetCookieForLocalDevHttp(proxyRes); + }); + }, + }; + } +} export default defineConfig({ plugins: [react()], server: { port: 5173, strictPort: true, - proxy: { - '/api': { - target: process.env.API_PROXY_TARGET || 'http://localhost:8787', - changeOrigin: true, - rewrite: (p) => p.replace(/^\/api/, ''), - configure: (proxy) => { - proxy.on('proxyReq', (proxyReq) => { - if (proxyBasicAuth) { - const b64 = Buffer.from(`${proxyBasicAuth.username}:${proxyBasicAuth.password}`, 'utf8').toString( - 'base64' - ); - proxyReq.setHeader('Authorization', `Basic ${b64}`); - return; - } - if (apiReadToken) proxyReq.setHeader('Authorization', `Bearer ${apiReadToken}`); - }); - }, - }, - }, + proxy, }, });