240 lines
8.1 KiB
TypeScript
240 lines
8.1 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { defineConfig, loadEnv } from 'vite';
|
|
import react from '@vitejs/plugin-react';
|
|
|
|
const DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(DIR, '../..');
|
|
|
|
type BasicAuth = { username: string; password: string };
|
|
|
|
function stripTrailingSlashes(p: string): string {
|
|
const out = p.replace(/\/+$/, '');
|
|
return out || '/';
|
|
}
|
|
|
|
function readApiReadToken(env: Record<string, string | undefined>): string | undefined {
|
|
if (env.API_READ_TOKEN) return env.API_READ_TOKEN;
|
|
const p = path.join(ROOT, 'tokens', 'read.json');
|
|
if (!fs.existsSync(p)) return undefined;
|
|
try {
|
|
const raw = fs.readFileSync(p, 'utf8');
|
|
const json = JSON.parse(raw) as { token?: string };
|
|
return json?.token ? String(json.token) : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function readApiAdminSecret(env: Record<string, string | undefined>): string | undefined {
|
|
if (Object.prototype.hasOwnProperty.call(env, 'API_PROXY_ADMIN_SECRET')) {
|
|
const raw = String(env.API_PROXY_ADMIN_SECRET || '').trim();
|
|
return raw || undefined;
|
|
}
|
|
const p = path.join(ROOT, 'tokens', 'api.json');
|
|
if (!fs.existsSync(p)) return undefined;
|
|
try {
|
|
const raw = fs.readFileSync(p, 'utf8');
|
|
const json = JSON.parse(raw) as { adminSecret?: string };
|
|
return json?.adminSecret ? String(json.adminSecret) : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function parseBasicAuth(value: string | undefined): BasicAuth | undefined {
|
|
const raw = String(value || '').trim();
|
|
if (!raw) return undefined;
|
|
const idx = raw.indexOf(':');
|
|
if (idx <= 0) return undefined;
|
|
const username = raw.slice(0, idx).trim();
|
|
const password = raw.slice(idx + 1);
|
|
if (!username || !password) return undefined;
|
|
return { username, password };
|
|
}
|
|
|
|
function readProxyBasicAuth(env: Record<string, string | undefined>): BasicAuth | undefined {
|
|
const fromEnv = parseBasicAuth(env.API_PROXY_BASIC_AUTH);
|
|
if (fromEnv) return fromEnv;
|
|
|
|
const fileRaw = String(env.API_PROXY_BASIC_AUTH_FILE || '').trim();
|
|
if (!fileRaw) return undefined;
|
|
|
|
const p = path.isAbsolute(fileRaw) ? fileRaw : path.join(ROOT, fileRaw);
|
|
if (!fs.existsSync(p)) return undefined;
|
|
try {
|
|
const raw = fs.readFileSync(p, 'utf8');
|
|
const json = JSON.parse(raw) as { username?: string; password?: string };
|
|
const username = typeof json?.username === 'string' ? json.username.trim() : '';
|
|
const password = typeof json?.password === 'string' ? json.password : '';
|
|
if (!username || !password) return undefined;
|
|
return { username, password };
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function parseUrl(v: string): URL | undefined {
|
|
try {
|
|
return new URL(v);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
function readDevProxyAuthUser(env: Record<string, string | undefined>): string | undefined {
|
|
const raw = String(env.DEV_PROXY_AUTH_USER || env.VITE_DEV_AUTH_USER || '').trim();
|
|
return raw || undefined;
|
|
}
|
|
|
|
export default defineConfig(({ mode }) => {
|
|
const env = { ...process.env, ...loadEnv(mode, DIR, '') } as Record<string, string | undefined>;
|
|
const apiReadToken = readApiReadToken(env);
|
|
const apiAdminSecret = readApiAdminSecret(env);
|
|
const proxyBasicAuth = readProxyBasicAuth(env);
|
|
const devProxyAuthUser = readDevProxyAuthUser(env);
|
|
const apiProxyTargetRaw = String(env.API_PROXY_TARGET || '').trim();
|
|
const apiProxyTarget = apiProxyTargetRaw || undefined;
|
|
const apiProxyTargetUrl = apiProxyTarget ? parseUrl(apiProxyTarget) : undefined;
|
|
const apiProxyTargetPath = stripTrailingSlashes(apiProxyTargetUrl?.pathname || '/');
|
|
const apiProxyTargetEndsWithApi = apiProxyTargetPath.endsWith('/api');
|
|
const apiProxyStripPrefixRaw = String(env.API_PROXY_STRIP_PREFIX || (apiProxyTargetEndsWithApi ? '/api' : '')).trim();
|
|
const apiProxyStripPrefix = apiProxyStripPrefixRaw ? stripTrailingSlashes(apiProxyStripPrefixRaw) : '';
|
|
const uiProxyTarget =
|
|
env.FRONTEND_PROXY_TARGET ||
|
|
env.UI_PROXY_TARGET ||
|
|
env.AUTH_PROXY_TARGET ||
|
|
inferUiProxyTarget(apiProxyTarget) ||
|
|
(apiProxyTargetUrl && apiProxyTargetPath === '/'
|
|
? stripTrailingSlashes(apiProxyTargetUrl.toString())
|
|
: undefined);
|
|
const graphqlProxyTarget = env.GRAPHQL_PROXY_TARGET || env.HASURA_PROXY_TARGET || uiProxyTarget;
|
|
const graphqlProxyPathRaw = String(env.GRAPHQL_PROXY_PATH || '').trim();
|
|
const graphqlProxyPath = graphqlProxyPathRaw ? stripTrailingSlashes(graphqlProxyPathRaw) : '';
|
|
const portRaw = env.VITE_DEV_PORT || env.DEV_PORT || '5173';
|
|
const port = Number.parseInt(String(portRaw), 10);
|
|
|
|
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 applyDevProxyAuth(proxyReq: any) {
|
|
if (!devProxyAuthUser) return false;
|
|
proxyReq.setHeader('x-trade-user', devProxyAuthUser);
|
|
return true;
|
|
}
|
|
|
|
const proxy: Record<string, any> = {
|
|
};
|
|
|
|
if (apiProxyTarget) {
|
|
proxy['/api'] = {
|
|
target: apiProxyTarget,
|
|
changeOrigin: true,
|
|
rewrite: (p: string) => {
|
|
if (!apiProxyStripPrefix) return p;
|
|
const rewritten = p.replace(new RegExp(`^${apiProxyStripPrefix}`), '');
|
|
return rewritten || '/';
|
|
},
|
|
configure: (p: any) => {
|
|
p.on('proxyReq', (proxyReq: any) => {
|
|
applyProxyBasicAuth(proxyReq);
|
|
applyDevProxyAuth(proxyReq);
|
|
if (apiAdminSecret) {
|
|
proxyReq.setHeader('x-admin-secret', apiAdminSecret);
|
|
return;
|
|
}
|
|
if (apiReadToken) proxyReq.setHeader('Authorization', `Bearer ${apiReadToken}`);
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
if (graphqlProxyTarget) {
|
|
for (const prefix of ['/graphql', '/graphql-ws']) {
|
|
proxy[prefix] = {
|
|
target: graphqlProxyTarget,
|
|
changeOrigin: true,
|
|
ws: true,
|
|
rewrite: (p: string) => (graphqlProxyPath ? p.replace(new RegExp(`^${prefix}`), graphqlProxyPath) : p),
|
|
configure: (p: any) => {
|
|
p.on('proxyReq', (proxyReq: any) => {
|
|
applyProxyBasicAuth(proxyReq);
|
|
applyDevProxyAuth(proxyReq);
|
|
});
|
|
p.on('proxyReqWs', (proxyReq: any) => {
|
|
applyProxyBasicAuth(proxyReq);
|
|
applyDevProxyAuth(proxyReq);
|
|
});
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
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);
|
|
applyDevProxyAuth(proxyReq);
|
|
});
|
|
p.on('proxyRes', (proxyRes: any) => {
|
|
rewriteSetCookieForLocalDevHttp(proxyRes);
|
|
});
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
plugins: [react()],
|
|
server: {
|
|
port: Number.isInteger(port) && port > 0 ? port : 5173,
|
|
strictPort: true,
|
|
proxy,
|
|
},
|
|
};
|
|
});
|