Migrate to Vite 7, improve UI (Copy/Download), and enhance API security
This commit is contained in:
25
Dockerfile
25
Dockerfile
@@ -14,31 +14,16 @@ COPY package*.json ./
|
|||||||
# Install dependencies (production + dev for build)
|
# Install dependencies (production + dev for build)
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
# Copy source code and build scripts
|
# Copy source code and build dependencies
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
COPY public/ ./public/
|
COPY public/ ./public/
|
||||||
COPY scripts/ ./scripts/
|
COPY scripts/ ./scripts/
|
||||||
COPY server.js ./server.js
|
COPY server.js ./server.js
|
||||||
|
COPY vite.config.js ./vite.config.js
|
||||||
|
COPY index.html ./index.html
|
||||||
|
|
||||||
# Generate version.js if version info provided, otherwise run normal build
|
# Build the application
|
||||||
RUN if [ -n "$VERSION" ]; then \
|
RUN npm run build
|
||||||
echo "// Auto-generated version file - do not edit manually" > src/version.js && \
|
|
||||||
echo "// Generated at: $(date -Iseconds)" >> src/version.js && \
|
|
||||||
echo "" >> src/version.js && \
|
|
||||||
echo "export const VERSION = '$VERSION';" >> src/version.js && \
|
|
||||||
echo "export const IS_RELEASE = $IS_RELEASE;" >> src/version.js && \
|
|
||||||
echo "export const BUILD_TIME = '$(date -Iseconds)';" >> src/version.js && \
|
|
||||||
echo "📝 Generated version.js with VERSION=$VERSION, IS_RELEASE=$IS_RELEASE"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build the application (skip prebuild if we already generated version.js)
|
|
||||||
RUN if [ -n "$VERSION" ]; then \
|
|
||||||
echo "🚀 Building with pre-generated version.js" && \
|
|
||||||
npx react-scripts build; \
|
|
||||||
else \
|
|
||||||
echo "🚀 Building with version-check.js" && \
|
|
||||||
npm run build; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM node:24-alpine AS production
|
FROM node:24-alpine AS production
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="JMESPath Testing Tool - Validate and test JMESPath expressions against JSON data" />
|
<meta name="description" content="JMESPath Testing Tool - Validate and test JMESPath expressions against JSON data" />
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700&family=Noto+Sans+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700&family=Noto+Sans+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||||
@@ -16,5 +15,6 @@
|
|||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
16134
package-lock.json
generated
16134
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -1,19 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "jmespath-playground",
|
"name": "jmespath-playground",
|
||||||
"version": "1.2.5",
|
"version": "1.3.0",
|
||||||
"description": "A React-based web application for testing JMESPath expressions against JSON data",
|
"description": "A React-based web application for testing JMESPath expressions against JSON data",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "vite",
|
||||||
"prebuild": "node scripts/version-check.js",
|
"prebuild": "node scripts/version-check.js",
|
||||||
"build": "react-scripts build",
|
"build": "vite build",
|
||||||
"test": "react-scripts test --watchAll=false",
|
"preview": "vite preview",
|
||||||
"test:watch": "react-scripts test",
|
"test": "vitest",
|
||||||
"server": "node server.js",
|
"server": "node server.js",
|
||||||
"dev": "concurrently \"npm start\" \"npm run server\"",
|
"dev": "concurrently \"npm start\" \"npm run server\"",
|
||||||
"build-image": "node scripts/build-image.js"
|
"build-image": "node scripts/build-image.js"
|
||||||
},
|
},
|
||||||
"proxy": "http://localhost:3000",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
},
|
},
|
||||||
@@ -26,21 +25,8 @@
|
|||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-scripts": "^5.0.1",
|
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"collectCoverageFrom": [
|
|
||||||
"src/**/*.{js,jsx,ts,tsx}",
|
|
||||||
"!src/index.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
@@ -63,7 +49,12 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
"@vitest/ui": "^4.0.18",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"supertest": "^7.2.2"
|
"jsdom": "^27.4.0",
|
||||||
|
"supertest": "^7.2.2",
|
||||||
|
"vite": "^7.3.1",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
336
server.js
336
server.js
@@ -1,9 +1,9 @@
|
|||||||
const express = require('express');
|
const express = require("express");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const crypto = require('crypto');
|
const crypto = require("crypto");
|
||||||
const os = require('os');
|
const os = require("os");
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require("uuid");
|
||||||
const { parseArgs } = require('util');
|
const { parseArgs } = require("util");
|
||||||
|
|
||||||
// Environment configuration
|
// Environment configuration
|
||||||
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS) || 100;
|
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS) || 100;
|
||||||
@@ -11,54 +11,29 @@ const MAX_SAMPLE_SIZE = parseInt(process.env.MAX_SAMPLE_SIZE) || 1024 * 1024; //
|
|||||||
const MAX_SESSION_TTL = parseInt(process.env.MAX_SESSION_TTL) || 60 * 60 * 1000; // 1 hour
|
const MAX_SESSION_TTL = parseInt(process.env.MAX_SESSION_TTL) || 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
// Utility functions for encryption
|
// Utility functions for encryption
|
||||||
function generateSalt() {
|
|
||||||
return crypto.randomBytes(16);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLocalhostRequest(req) {
|
|
||||||
// Get client IP with fallback options
|
|
||||||
const forwarded = req.get('X-Forwarded-For');
|
|
||||||
const ip = forwarded ? forwarded.split(',')[0].trim() :
|
|
||||||
req.ip ||
|
|
||||||
req.connection.remoteAddress ||
|
|
||||||
req.socket.remoteAddress ||
|
|
||||||
'127.0.0.1';
|
|
||||||
|
|
||||||
const host = req.get('host') || '';
|
|
||||||
|
|
||||||
// Check for localhost IP addresses (IPv4 and IPv6)
|
|
||||||
const localhostIPs = ['127.0.0.1', '::1', '::ffff:127.0.0.1', 'localhost'];
|
|
||||||
const isLocalIP = localhostIPs.includes(ip) || ip.startsWith('127.') || ip === '::1';
|
|
||||||
|
|
||||||
// Check for localhost hostnames
|
|
||||||
const isLocalHost = host.startsWith('localhost:') || host.startsWith('127.0.0.1:') || host === 'localhost' || host === '127.0.0.1';
|
|
||||||
|
|
||||||
return isLocalIP || isLocalHost;
|
|
||||||
}
|
|
||||||
|
|
||||||
function encrypt(data, key) {
|
function encrypt(data, key) {
|
||||||
try {
|
try {
|
||||||
const algorithm = 'aes-256-gcm';
|
const algorithm = "aes-256-gcm";
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||||
cipher.setAAD(Buffer.from('session-data'));
|
cipher.setAAD(Buffer.from("session-data"));
|
||||||
|
|
||||||
let encrypted = cipher.update(JSON.stringify(data), 'utf8');
|
let encrypted = cipher.update(JSON.stringify(data), "utf8");
|
||||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||||
|
|
||||||
const authTag = cipher.getAuthTag();
|
const authTag = cipher.getAuthTag();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
iv: iv.toString('hex'),
|
iv: iv.toString("hex"),
|
||||||
data: encrypted.toString('hex'),
|
data: encrypted.toString("hex"),
|
||||||
tag: authTag.toString('hex')
|
tag: authTag.toString("hex"),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('⚠️ Encryption exception:', {
|
console.error("⚠️ Encryption exception:", {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
algorithm: 'aes-256-gcm',
|
algorithm: "aes-256-gcm",
|
||||||
keyLength: key ? key.length : 'undefined',
|
keyLength: key ? key.length : "undefined",
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
throw new Error(`Encryption failed: ${error.message}`);
|
throw new Error(`Encryption failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -66,39 +41,40 @@ function encrypt(data, key) {
|
|||||||
|
|
||||||
function decrypt(encryptedObj, key) {
|
function decrypt(encryptedObj, key) {
|
||||||
try {
|
try {
|
||||||
const algorithm = 'aes-256-gcm';
|
const algorithm = "aes-256-gcm";
|
||||||
const iv = Buffer.from(encryptedObj.iv, 'hex');
|
const iv = Buffer.from(encryptedObj.iv, "hex");
|
||||||
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
||||||
decipher.setAAD(Buffer.from('session-data'));
|
decipher.setAAD(Buffer.from("session-data"));
|
||||||
decipher.setAuthTag(Buffer.from(encryptedObj.tag, 'hex'));
|
decipher.setAuthTag(Buffer.from(encryptedObj.tag, "hex"));
|
||||||
|
|
||||||
let decrypted = decipher.update(Buffer.from(encryptedObj.data, 'hex'), null, 'utf8');
|
let decrypted = decipher.update(
|
||||||
decrypted += decipher.final('utf8');
|
Buffer.from(encryptedObj.data, "hex"),
|
||||||
|
null,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
decrypted += decipher.final("utf8");
|
||||||
|
|
||||||
return JSON.parse(decrypted);
|
return JSON.parse(decrypted);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('⚠️ Decryption exception:', {
|
console.error("⚠️ Decryption exception:", {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
algorithm: 'aes-256-gcm',
|
algorithm: "aes-256-gcm",
|
||||||
keyLength: key ? key.length : 'undefined',
|
keyLength: key ? key.length : "undefined",
|
||||||
hasIV: !!encryptedObj.iv,
|
hasIV: !!encryptedObj.iv,
|
||||||
hasTag: !!encryptedObj.tag,
|
hasTag: !!encryptedObj.tag,
|
||||||
hasData: !!encryptedObj.data,
|
hasData: !!encryptedObj.data,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
throw new Error(`Decryption failed: ${error.message}`);
|
throw new Error(`Decryption failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For localhost requests, use a consistent API key so sessions persist
|
|
||||||
const LOCALHOST_API_KEY = 'localhost0123456789abcdef0123456789';
|
|
||||||
|
|
||||||
function isValidApiKey(apiKey) {
|
function isValidApiKey(apiKey) {
|
||||||
return typeof apiKey === 'string' && /^[0-9a-f]{32}$/i.test(apiKey);
|
return typeof apiKey === "string" && /^[0-9a-f]{32}$/i.test(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSessionId(apiKey) {
|
function getSessionId(apiKey) {
|
||||||
return crypto.createHash('sha256').update(apiKey).digest('hex');
|
return crypto.createHash("sha256").update(apiKey).digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSalt() {
|
function generateSalt() {
|
||||||
@@ -106,7 +82,7 @@ function generateSalt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deriveKey(apiKey, salt) {
|
function deriveKey(apiKey, salt) {
|
||||||
return crypto.pbkdf2Sync(apiKey, salt, 10000, 32, 'sha256');
|
return crypto.pbkdf2Sync(apiKey, salt, 100000, 32, "sha256");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Express app
|
// Create Express app
|
||||||
@@ -114,24 +90,24 @@ function createApp(devMode = false) {
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Trust proxy to get real client IP (needed for localhost detection)
|
// Trust proxy to get real client IP (needed for localhost detection)
|
||||||
app.set('trust proxy', true);
|
app.set("trust proxy", true);
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(express.json({ limit: MAX_SAMPLE_SIZE }));
|
app.use(express.json({ limit: MAX_SAMPLE_SIZE }));
|
||||||
app.use(express.static(path.join(__dirname, 'build')));
|
app.use(express.static(path.join(__dirname, "build")));
|
||||||
|
|
||||||
// Dev mode request logging middleware
|
// Dev mode request logging middleware
|
||||||
if (devMode) {
|
if (devMode) {
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
console.log(`📨 [${timestamp}] ${req.method} ${req.path}`);
|
console.log(`📨 [${timestamp}] ${req.method} ${req.path}`);
|
||||||
if (req.method !== 'GET' && Object.keys(req.body).length > 0) {
|
if (req.method !== "GET" && Object.keys(req.body).length > 0) {
|
||||||
const bodySize = Buffer.byteLength(JSON.stringify(req.body), 'utf8');
|
const bodySize = Buffer.byteLength(JSON.stringify(req.body), "utf8");
|
||||||
console.log(` Request body size: ${(bodySize / 1024).toFixed(2)}KB`);
|
console.log(` Request body size: ${(bodySize / 1024).toFixed(2)}KB`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalJson = res.json;
|
const originalJson = res.json;
|
||||||
res.json = function(data) {
|
res.json = function (data) {
|
||||||
console.log(` ✓ Response: ${res.statusCode}`);
|
console.log(` ✓ Response: ${res.statusCode}`);
|
||||||
return originalJson.call(this, data);
|
return originalJson.call(this, data);
|
||||||
};
|
};
|
||||||
@@ -148,7 +124,9 @@ function createApp(devMode = false) {
|
|||||||
for (const [sessionId, session] of sessions.entries()) {
|
for (const [sessionId, session] of sessions.entries()) {
|
||||||
if (now - session.createdAt > MAX_SESSION_TTL) {
|
if (now - session.createdAt > MAX_SESSION_TTL) {
|
||||||
sessions.delete(sessionId);
|
sessions.delete(sessionId);
|
||||||
console.log(`🧹 Cleaned up expired session: ${sessionId.substring(0, 8)}...`);
|
console.log(
|
||||||
|
`🧹 Cleaned up expired session: ${sessionId.substring(0, 8)}...`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,22 +135,15 @@ function createApp(devMode = false) {
|
|||||||
setInterval(cleanupExpiredSessions, 5 * 60 * 1000);
|
setInterval(cleanupExpiredSessions, 5 * 60 * 1000);
|
||||||
|
|
||||||
// API endpoints
|
// API endpoints
|
||||||
app.post('/api/v1/upload', (req, res) => {
|
app.post("/api/v1/upload", (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Check if request is from localhost - if so, skip API key validation
|
const apiKey = req.headers["x-api-key"];
|
||||||
const isFromLocalhost = isLocalhostRequest(req);
|
|
||||||
let apiKey = req.headers['x-api-key'];
|
|
||||||
|
|
||||||
if (!isFromLocalhost) {
|
// Validate API key header
|
||||||
// Validate API key header for remote clients
|
if (!apiKey || !isValidApiKey(apiKey)) {
|
||||||
if (!apiKey || !isValidApiKey(apiKey)) {
|
return res
|
||||||
return res.status(403).json({ error: 'Invalid or missing X-API-Key header' });
|
.status(403)
|
||||||
}
|
.json({ error: "Invalid or missing X-API-Key header" });
|
||||||
} else {
|
|
||||||
// For localhost requests, use consistent API key for session persistence
|
|
||||||
if (!apiKey || !isValidApiKey(apiKey)) {
|
|
||||||
apiKey = LOCALHOST_API_KEY;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup expired sessions before checking limits
|
// Cleanup expired sessions before checking limits
|
||||||
@@ -181,26 +152,26 @@ function createApp(devMode = false) {
|
|||||||
// Check session limits
|
// Check session limits
|
||||||
if (sessions.size >= MAX_SESSIONS) {
|
if (sessions.size >= MAX_SESSIONS) {
|
||||||
return res.status(429).json({
|
return res.status(429).json({
|
||||||
error: 'Maximum number of sessions reached. Please try again later.',
|
error: "Maximum number of sessions reached. Please try again later.",
|
||||||
maxSessions: MAX_SESSIONS,
|
maxSessions: MAX_SESSIONS,
|
||||||
currentSessions: sessions.size
|
currentSessions: sessions.size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedData = req.body;
|
const uploadedData = req.body;
|
||||||
|
|
||||||
// Validate that it's valid JSON
|
// Validate that it's valid JSON
|
||||||
if (!uploadedData || typeof uploadedData !== 'object') {
|
if (!uploadedData || typeof uploadedData !== "object") {
|
||||||
return res.status(400).json({ error: 'Invalid JSON data' });
|
return res.status(400).json({ error: "Invalid JSON data" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check data size
|
// Check data size
|
||||||
const dataSize = Buffer.byteLength(JSON.stringify(uploadedData), 'utf8');
|
const dataSize = Buffer.byteLength(JSON.stringify(uploadedData), "utf8");
|
||||||
if (dataSize > MAX_SAMPLE_SIZE) {
|
if (dataSize > MAX_SAMPLE_SIZE) {
|
||||||
return res.status(413).json({
|
return res.status(413).json({
|
||||||
error: 'Sample data too large',
|
error: "Sample data too large",
|
||||||
maxSize: MAX_SAMPLE_SIZE,
|
maxSize: MAX_SAMPLE_SIZE,
|
||||||
receivedSize: dataSize
|
receivedSize: dataSize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,69 +184,66 @@ function createApp(devMode = false) {
|
|||||||
const encryptedData = encrypt(uploadedData, key);
|
const encryptedData = encrypt(uploadedData, key);
|
||||||
|
|
||||||
sessions.set(sessionId, {
|
sessions.set(sessionId, {
|
||||||
salt: salt.toString('hex'),
|
salt: salt.toString("hex"),
|
||||||
encryptedData,
|
encryptedData,
|
||||||
state: stateGuid,
|
state: stateGuid,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
accessed: false
|
accessed: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`);
|
console.log(
|
||||||
|
`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`,
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: 'Sample data uploaded successfully',
|
message: "Sample data uploaded successfully",
|
||||||
state: stateGuid,
|
state: stateGuid,
|
||||||
sessionId: sessionId.substring(0, 8) + '...'
|
sessionId: sessionId.substring(0, 8) + "...",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('⚠️ Upload endpoint exception occurred:', {
|
console.error("⚠️ Upload endpoint exception occurred:", {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
sessionCount: sessions.size,
|
sessionCount: sessions.size,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provide more specific error messages based on error type
|
// Provide more specific error messages based on error type
|
||||||
if (error.name === 'SyntaxError') {
|
if (error.name === "SyntaxError") {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'Invalid JSON data format',
|
error: "Invalid JSON data format",
|
||||||
details: 'The uploaded data could not be parsed as valid JSON'
|
details: "The uploaded data could not be parsed as valid JSON",
|
||||||
});
|
});
|
||||||
} else if (error.message.includes('encrypt')) {
|
} else if (error.message.includes("encrypt")) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: 'Encryption failed',
|
error: "Encryption failed",
|
||||||
details: 'Failed to encrypt session data. Please try again with a new API key.'
|
details:
|
||||||
|
"Failed to encrypt session data. Please try again with a new API key.",
|
||||||
});
|
});
|
||||||
} else if (error.message.includes('PBKDF2')) {
|
} else if (error.message.includes("PBKDF2")) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: 'Key derivation failed',
|
error: "Key derivation failed",
|
||||||
details: 'Failed to derive encryption key from API key'
|
details: "Failed to derive encryption key from API key",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: 'Upload processing failed',
|
error: "Upload processing failed",
|
||||||
details: 'An unexpected error occurred while processing your upload. Please try again.'
|
details:
|
||||||
|
"An unexpected error occurred while processing your upload. Please try again.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/v1/sample', (req, res) => {
|
app.get("/api/v1/sample", (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Check if request is from localhost - if so, skip API key validation
|
const apiKey = req.headers["x-api-key"];
|
||||||
const isFromLocalhost = isLocalhostRequest(req);
|
|
||||||
let apiKey = req.headers['x-api-key'];
|
|
||||||
|
|
||||||
if (!isFromLocalhost) {
|
// Validate API key header
|
||||||
// Validate API key header for remote clients
|
if (!apiKey || !isValidApiKey(apiKey)) {
|
||||||
if (!apiKey || !isValidApiKey(apiKey)) {
|
return res
|
||||||
return res.status(403).json({ error: 'Invalid or missing X-API-Key header' });
|
.status(403)
|
||||||
}
|
.json({ error: "Invalid or missing X-API-Key header" });
|
||||||
} else {
|
|
||||||
// For localhost requests, use consistent API key for session persistence
|
|
||||||
if (!apiKey || !isValidApiKey(apiKey)) {
|
|
||||||
apiKey = LOCALHOST_API_KEY;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = getSessionId(apiKey);
|
const sessionId = getSessionId(apiKey);
|
||||||
@@ -286,64 +254,62 @@ function createApp(devMode = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt data
|
// Decrypt data
|
||||||
const salt = Buffer.from(session.salt, 'hex');
|
const salt = Buffer.from(session.salt, "hex");
|
||||||
const key = deriveKey(apiKey, salt);
|
const key = deriveKey(apiKey, salt);
|
||||||
const decryptedData = decrypt(session.encryptedData, key);
|
const decryptedData = decrypt(session.encryptedData, key);
|
||||||
|
|
||||||
// Remove session after first access (one-time use)
|
// Remove session after first access (one-time use)
|
||||||
sessions.delete(sessionId);
|
sessions.delete(sessionId);
|
||||||
console.log(`📤 Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`);
|
console.log(
|
||||||
|
`📤 Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`,
|
||||||
|
);
|
||||||
|
|
||||||
res.json(decryptedData);
|
res.json(decryptedData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('⚠️ Sample retrieval exception occurred:', {
|
console.error("⚠️ Sample retrieval exception occurred:", {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
sessionCount: sessions.size,
|
sessionCount: sessions.size,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provide more specific error messages based on error type
|
// Provide more specific error messages based on error type
|
||||||
if (error.message.includes('decrypt')) {
|
if (error.message.includes("decrypt")) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: 'Decryption failed',
|
error: "Decryption failed",
|
||||||
details: 'Failed to decrypt session data. The session may be corrupted or the API key may be incorrect.'
|
details:
|
||||||
|
"Failed to decrypt session data. The session may be corrupted or the API key may be incorrect.",
|
||||||
});
|
});
|
||||||
} else if (error.message.includes('JSON')) {
|
} else if (error.message.includes("JSON")) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: 'Data corruption detected',
|
error: "Data corruption detected",
|
||||||
details: 'The stored session data appears to be corrupted and cannot be parsed.'
|
details:
|
||||||
|
"The stored session data appears to be corrupted and cannot be parsed.",
|
||||||
});
|
});
|
||||||
} else if (error.name === 'TypeError') {
|
} else if (error.name === "TypeError") {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: 'Session data format error',
|
error: "Session data format error",
|
||||||
details: 'The session data format is invalid or corrupted.'
|
details: "The session data format is invalid or corrupted.",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: 'Sample retrieval failed',
|
error: "Sample retrieval failed",
|
||||||
details: 'An unexpected error occurred while retrieving sample data. The session may have been corrupted.'
|
details:
|
||||||
|
"An unexpected error occurred while retrieving sample data. The session may have been corrupted.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/v1/state', (req, res) => {
|
app.get("/api/v1/state", (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Check if request is from localhost - if so, skip API key validation
|
const apiKey = req.headers["x-api-key"];
|
||||||
const isFromLocalhost = isLocalhostRequest(req);
|
|
||||||
let apiKey = req.headers['x-api-key'];
|
|
||||||
|
|
||||||
if (!isFromLocalhost) {
|
// Validate API key header
|
||||||
// Validate API key header for remote clients
|
if (!apiKey || !isValidApiKey(apiKey)) {
|
||||||
if (!apiKey || !isValidApiKey(apiKey)) {
|
return res
|
||||||
return res.status(403).json({ error: 'Invalid or missing X-API-Key header' });
|
.status(403)
|
||||||
}
|
.json({ error: "Invalid or missing X-API-Key header" });
|
||||||
} else {
|
|
||||||
// For localhost requests, use consistent API key for session persistence
|
|
||||||
if (!apiKey || !isValidApiKey(apiKey)) {
|
|
||||||
apiKey = LOCALHOST_API_KEY;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = getSessionId(apiKey);
|
const sessionId = getSessionId(apiKey);
|
||||||
@@ -356,55 +322,56 @@ function createApp(devMode = false) {
|
|||||||
|
|
||||||
res.json({ state: session.state });
|
res.json({ state: session.state });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('⚠️ State retrieval exception occurred:', {
|
console.error("⚠️ State retrieval exception occurred:", {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
sessionCount: sessions.size,
|
sessionCount: sessions.size,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provide more specific error messages
|
// Provide more specific error messages
|
||||||
if (error.message.includes('API key')) {
|
if (error.message.includes("API key")) {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
error: 'API key processing failed',
|
error: "API key processing failed",
|
||||||
details: 'Failed to process the provided API key'
|
details: "Failed to process the provided API key",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: 'State retrieval failed',
|
error: "State retrieval failed",
|
||||||
details: 'An unexpected error occurred while retrieving session state. Please try again.'
|
details:
|
||||||
|
"An unexpected error occurred while retrieving session state. Please try again.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status endpoint (no auth required) - detailed information
|
// Status endpoint (no auth required) - detailed information
|
||||||
app.get('/api/v1/status', (req, res) => {
|
app.get("/api/v1/status", (req, res) => {
|
||||||
cleanupExpiredSessions(); // Cleanup on status check
|
cleanupExpiredSessions(); // Cleanup on status check
|
||||||
res.json({
|
res.json({
|
||||||
status: 'healthy',
|
status: "healthy",
|
||||||
sessions: {
|
sessions: {
|
||||||
current: sessions.size,
|
current: sessions.size,
|
||||||
max: MAX_SESSIONS,
|
max: MAX_SESSIONS,
|
||||||
available: MAX_SESSIONS - sessions.size
|
available: MAX_SESSIONS - sessions.size,
|
||||||
},
|
},
|
||||||
limits: {
|
limits: {
|
||||||
maxSessions: MAX_SESSIONS,
|
maxSessions: MAX_SESSIONS,
|
||||||
maxSampleSize: MAX_SAMPLE_SIZE,
|
maxSampleSize: MAX_SAMPLE_SIZE,
|
||||||
maxSessionTTL: MAX_SESSION_TTL
|
maxSessionTTL: MAX_SESSION_TTL,
|
||||||
},
|
},
|
||||||
uptime: process.uptime()
|
uptime: process.uptime(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Health endpoint (no auth required) - simple OK response
|
// Health endpoint (no auth required) - simple OK response
|
||||||
app.get('/api/v1/health', (req, res) => {
|
app.get("/api/v1/health", (req, res) => {
|
||||||
res.type('text/plain').send('OK');
|
res.type("text/plain").send("OK");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Serve React app for all other routes
|
// Serve React app for all other routes
|
||||||
app.get('*', (req, res) => {
|
app.get("*", (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'build', 'index.html'));
|
res.sendFile(path.join(__dirname, "build", "index.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
@@ -414,16 +381,27 @@ function createApp(devMode = false) {
|
|||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const { values } = parseArgs({
|
const { values } = parseArgs({
|
||||||
options: {
|
options: {
|
||||||
'listen-addr': { type: 'string', short: 'h', default: process.env.LISTEN_ADDR || '127.0.0.1' },
|
"listen-addr": {
|
||||||
'port': { type: 'string', short: 'p', default: process.env.LISTEN_PORT || '3000' },
|
type: "string",
|
||||||
'dev': { type: 'boolean', default: process.env.DEV_MODE === 'true' || false }
|
short: "h",
|
||||||
}
|
default: process.env.LISTEN_ADDR || "127.0.0.1",
|
||||||
|
},
|
||||||
|
port: {
|
||||||
|
type: "string",
|
||||||
|
short: "p",
|
||||||
|
default: process.env.LISTEN_PORT || "3000",
|
||||||
|
},
|
||||||
|
dev: {
|
||||||
|
type: "boolean",
|
||||||
|
default: process.env.DEV_MODE === "true" || false,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const DEV_MODE = values.dev;
|
const DEV_MODE = values.dev;
|
||||||
const app = createApp(DEV_MODE);
|
const app = createApp(DEV_MODE);
|
||||||
const PORT = parseInt(values.port);
|
const PORT = parseInt(values.port);
|
||||||
const HOST = values['listen-addr'];
|
const HOST = values["listen-addr"];
|
||||||
|
|
||||||
app.listen(PORT, HOST, () => {
|
app.listen(PORT, HOST, () => {
|
||||||
console.log(`JMESPath Playground Server running`);
|
console.log(`JMESPath Playground Server running`);
|
||||||
@@ -432,12 +410,12 @@ if (require.main === module) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show actual accessible URLs
|
// Show actual accessible URLs
|
||||||
if (HOST === '0.0.0.0') {
|
if (HOST === "0.0.0.0") {
|
||||||
console.log(` Listening on all interfaces:`);
|
console.log(` Listening on all interfaces:`);
|
||||||
const interfaces = os.networkInterfaces();
|
const interfaces = os.networkInterfaces();
|
||||||
for (const [name, addrs] of Object.entries(interfaces)) {
|
for (const [name, addrs] of Object.entries(interfaces)) {
|
||||||
for (const addr of addrs) {
|
for (const addr of addrs) {
|
||||||
if (addr.family === 'IPv4' && !addr.internal) {
|
if (addr.family === "IPv4" && !addr.internal) {
|
||||||
console.log(` http://${addr.address}:${PORT}`);
|
console.log(` http://${addr.address}:${PORT}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,17 +428,24 @@ if (require.main === module) {
|
|||||||
|
|
||||||
console.log(`Configuration:`);
|
console.log(`Configuration:`);
|
||||||
console.log(` Max Sessions: ${MAX_SESSIONS}`);
|
console.log(` Max Sessions: ${MAX_SESSIONS}`);
|
||||||
console.log(` Max Sample Size: ${(MAX_SAMPLE_SIZE / 1024 / 1024).toFixed(1)}MB`);
|
console.log(
|
||||||
console.log(` Session TTL: ${(MAX_SESSION_TTL / 1000 / 60).toFixed(0)} minutes`);
|
` Max Sample Size: ${(MAX_SAMPLE_SIZE / 1024 / 1024).toFixed(1)}MB`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` Session TTL: ${(MAX_SESSION_TTL / 1000 / 60).toFixed(0)} minutes`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" Security: AES-256-GCM encryption with PBKDF2 (100k iterations)",
|
||||||
|
);
|
||||||
|
|
||||||
// Show base API URL
|
// Show base API URL
|
||||||
let apiBaseUrl;
|
let apiBaseUrl;
|
||||||
if (HOST === '0.0.0.0') {
|
if (HOST === "0.0.0.0") {
|
||||||
const interfaces = os.networkInterfaces();
|
const interfaces = os.networkInterfaces();
|
||||||
let firstIP = '127.0.0.1';
|
let firstIP = "127.0.0.1";
|
||||||
outer: for (const addrs of Object.values(interfaces)) {
|
outer: for (const addrs of Object.values(interfaces)) {
|
||||||
for (const addr of addrs) {
|
for (const addr of addrs) {
|
||||||
if (addr.family === 'IPv4' && !addr.internal) {
|
if (addr.family === "IPv4" && !addr.internal) {
|
||||||
firstIP = addr.address;
|
firstIP = addr.address;
|
||||||
break outer;
|
break outer;
|
||||||
}
|
}
|
||||||
@@ -472,8 +457,7 @@ if (require.main === module) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`API Base URL: ${apiBaseUrl}`);
|
console.log(`API Base URL: ${apiBaseUrl}`);
|
||||||
console.log(`Security: AES-256-GCM encryption with PBKDF2 key derivation`);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createApp };
|
module.exports = { createApp };
|
||||||
|
|||||||
@@ -83,22 +83,10 @@ function App() {
|
|||||||
|
|
||||||
// Get headers for API requests
|
// Get headers for API requests
|
||||||
const getApiHeaders = () => {
|
const getApiHeaders = () => {
|
||||||
const headers = {
|
return {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
|
"X-API-Key": apiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only send API key for non-localhost requests
|
|
||||||
// For localhost, let server use its default LOCALHOST_API_KEY
|
|
||||||
if (
|
|
||||||
window.location.hostname !== "localhost" &&
|
|
||||||
window.location.hostname !== "127.0.0.1" &&
|
|
||||||
!window.location.hostname.startsWith("127.") &&
|
|
||||||
window.location.hostname !== "::1"
|
|
||||||
) {
|
|
||||||
headers["X-API-Key"] = apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load sample data from API on startup and setup periodic state checking
|
// Load sample data from API on startup and setup periodic state checking
|
||||||
@@ -1,13 +1,27 @@
|
|||||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
// Mock localStorage
|
||||||
|
const localStorageMock = (function() {
|
||||||
|
let store = {};
|
||||||
|
return {
|
||||||
|
getItem: vi.fn((key) => store[key] || null),
|
||||||
|
setItem: vi.fn((key, value) => { store[key] = value.toString(); }),
|
||||||
|
clear: vi.fn(() => { store = {}; }),
|
||||||
|
removeItem: vi.fn((key) => { delete store[key]; })
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||||
|
|
||||||
// Mock fetch for API calls
|
// Mock fetch for API calls
|
||||||
global.fetch = jest.fn();
|
global.fetch = vi.fn();
|
||||||
|
|
||||||
describe('App Component', () => {
|
describe('App Component', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetch.mockClear();
|
vi.clearAllMocks();
|
||||||
// Mock successful API responses
|
// Mock successful API responses
|
||||||
fetch.mockImplementation((url) => {
|
fetch.mockImplementation((url) => {
|
||||||
if (url.includes('/api/v1/sample')) {
|
if (url.includes('/api/v1/sample')) {
|
||||||
@@ -55,8 +55,7 @@ function ApiKeyPage({ apiKey, onRegenerateApiKey }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-text">
|
<div className="form-text">
|
||||||
This API key is used to encrypt and authenticate data uploads from remote clients.
|
This API key is used to encrypt and authenticate data uploads.
|
||||||
<strong>Note:</strong> Requests from localhost (127.0.0.1) do not require an API key.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ function ApiKeyPage({ apiKey, onRegenerateApiKey }) {
|
|||||||
<h6>📡 Remote Data Upload API</h6>
|
<h6>📡 Remote Data Upload API</h6>
|
||||||
<p className="text-muted">
|
<p className="text-muted">
|
||||||
External tools can upload sample data remotely using the REST API.
|
External tools can upload sample data remotely using the REST API.
|
||||||
For remote clients, the API key is required for authentication. Define two
|
The API key is required for authentication. Define two
|
||||||
environment variables in your <code>.bashrc</code>.
|
environment variables in your <code>.bashrc</code>.
|
||||||
</p>
|
</p>
|
||||||
<pre className="bg-light p-3 rounded border">
|
<pre className="bg-light p-3 rounded border">
|
||||||
@@ -80,10 +79,9 @@ function ApiKeyPage({ apiKey, onRegenerateApiKey }) {
|
|||||||
"$\{JMESPATH_PLAYGROUND_API_URL}/api/v1/upload"`}</code>
|
"$\{JMESPATH_PLAYGROUND_API_URL}/api/v1/upload"`}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<div className="form-text">
|
<div className="form-text">
|
||||||
Replace <code>{'__JSON_FILE_NAME__'}</code> with the path to your JSON file containing the sample data.
|
Replace <code>{"__JSON_FILE_NAME__"}</code> with the path to your
|
||||||
or use <code>-</code> to read from standard input.
|
JSON file containing the sample data. or use <code>-</code> to
|
||||||
<br />
|
read from standard input.
|
||||||
<strong>For localhost clients:</strong> The X-API-Key should be omitted.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
|
|
||||||
// Add TextEncoder/TextDecoder for Node.js compatibility
|
|
||||||
if (typeof TextEncoder === 'undefined') {
|
|
||||||
global.TextEncoder = require('util').TextEncoder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof TextDecoder === 'undefined') {
|
|
||||||
global.TextDecoder = require('util').TextDecoder;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock crypto.getRandomValues for test environment
|
|
||||||
if (typeof global.crypto === 'undefined') {
|
|
||||||
global.crypto = {
|
|
||||||
getRandomValues: (array) => {
|
|
||||||
// Simple predictable mock for testing
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
|
||||||
array[i] = Math.floor(Math.random() * 256);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress console errors during tests
|
|
||||||
const originalError = console.error;
|
|
||||||
beforeAll(() => {
|
|
||||||
console.error = (...args) => {
|
|
||||||
if (
|
|
||||||
typeof args[0] === 'string' &&
|
|
||||||
(args[0].includes('Warning: ReactDOMTestUtils.act is deprecated') ||
|
|
||||||
args[0].includes('Warning: An update to App inside a test was not wrapped in act'))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
originalError.call(console, ...args);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
console.error = originalError;
|
|
||||||
});
|
|
||||||
23
vite.config.js
Normal file
23
vite.config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'build',
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
css: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user