diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index de4934c..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,84 +0,0 @@ -# Development Guide - -## Quick Start Commands - -```bash -# Install dependencies -npm install - -# Start development server (with hot reload) -npm start - -# Build for production -npm run build - -# Run tests -npm test - -# Serve production build locally -npm run serve -``` - -## Docker Commands - -```bash -# Build Docker container -docker build -t jmespath-playground . - -# Run Docker container -docker run -p 3000:3000 jmespath-playground - -# Development with Docker Compose -docker-compose --profile dev up jmespath-playground-dev - -# Production with Docker Compose -docker-compose up jmespath-playground -``` - -## Project Structure Overview - -``` -src/ -├── App.js # Main component with JMESPath logic -├── App.css # App-specific styles -├── App.test.js # Basic tests -├── index.js # React entry point -├── index.css # Global styles and Bootstrap overrides -└── setupTests.js # Test configuration -``` - -## Key Features Implemented - -1. **Real-time JMESPath Evaluation**: Uses the `jmespath` library to evaluate expressions as user types -2. **JSON Validation**: Parses and validates JSON input with error reporting -3. **Bootstrap UI**: Responsive layout with cards, buttons, and form controls -4. **Error Handling**: Clear error messages for both JSON and JMESPath syntax issues -5. **Sample Data**: Pre-loaded examples with "Load Sample" button -6. **JSON Formatting**: "Format JSON" button to prettify JSON input -7. **Clear Function**: "Clear All" button to reset all inputs - -## Component Architecture - -The main `App.js` component manages: -- State for JMESPath expression, JSON data, results, and errors -- Auto-evaluation using `useEffect` when inputs change -- Error handling for both JSON parsing and JMESPath evaluation -- UI event handlers for buttons and input changes - -## Styling - -- Bootstrap 5.3.2 for responsive grid and components -- Custom CSS for enhanced UX (color coding, hover effects) -- Gradient header with professional appearance -- Color-coded input areas (yellow for JMESPath, green for JSON, blue for results) - -## Browser Compatibility - -Built with React 18 and targets: -- Modern evergreen browsers -- Node.js 24 LTS compatibility -- Mobile-responsive design - -## License - -MIT License - see LICENSE file for details. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 1c4e24f..9b06372 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,6 +56,10 @@ RUN npm ci --only=production && npm cache clean --force COPY --from=builder /app/build ./build COPY --from=builder /app/server.js ./server.js +# Copy entrypoint script +COPY entrypoint.sh ./entrypoint.sh +RUN chmod +x entrypoint.sh + # Expose port 3000 EXPOSE 3000 @@ -64,4 +68,4 @@ ENV LISTEN_ADDR=0.0.0.0 ENV LISTEN_PORT=3000 # Start the integrated server -CMD ["node", "server.js"] \ No newline at end of file +ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 10f4923..ae92d79 100644 --- a/README.md +++ b/README.md @@ -8,62 +8,53 @@ A React-based web application for testing and validating JMESPath expressions ag ## Features -- 🎯 **Real-time Evaluation**: JMESPath expressions are evaluated instantly as you type -- 📝 **JSON Validation**: Built-in JSON syntax validation and error reporting -- 📁 **File Upload**: Load JSON data directly from local files (supports JSON Lines format for .log files) -- 🎨 **Bootstrap UI**: Clean, responsive interface with Bootstrap styling -- 🔄 **Sample Data**: Pre-loaded examples to get started quickly -- 📱 **Responsive Design**: Works on desktop, tablet, and mobile devices -- 🐳 **Docker Ready**: Containerized for easy deployment -- ✅ **Error Handling**: Clear error messages for both JSON and JMESPath syntax issues - -## Application Layout - -The application is divided into three main sections: - -1. **Top Section**: Title and description of the tool -2. **Middle Section**: Input area for JMESPath expressions -3. **Bottom Sections**: - - **Left**: JSON data input area - - **Right**: Query results output area +- **Real-time Evaluation**: JMESPath expressions are evaluated instantly as you type +- **File Upload**: Load JSON data directly from local files (supports JSON Lines format for .log files) +- **Remote API**: Upload sample data remotely via REST API with encrypted sessions +- **Container Ready**: Containerized for easy deployment ## Quick Start ### Prerequisites - Node.js 24 LTS or higher -- npm or yarn package manager +- npm package manager ### Local Development 1. **Clone the repository**: + ```bash git clone cd jmespath-playground ``` 2. **Install dependencies**: + ```bash npm install ``` 3. **Start the development server**: + ```bash npm start ``` 4. **Open your browser** and navigate to `http://localhost:3000` -### Container Deployment (Optional) +### Container Deployment -You can optionally run the application in a Docker container: +You can optionally run the application in a container: ```bash -# Build the Docker image -docker build -t jmespath-playground . +# Build the container image +npm run build-image -# Run the container +# Run the container (Docker or Apple Container Tools) docker run -p 3000:3000 jmespath-playground +# or +container run -p 3000:3000 jmespath-playground ``` ## Usage @@ -82,93 +73,56 @@ docker run -p 3000:3000 jmespath-playground - Format JSON for better readability - Clear all inputs -### Example JMESPath Expressions +## Remote API Usage -Try these examples with the sample data: +The application includes a REST API for uploading sample data remotely: -- `people[*].name` - Get all names -- `people[0]` - Get the first person -- `people[?age > 30]` - Filter people older than 30 -- `people[*].skills[0]` - Get the first skill of each person -- `length(people)` - Count the number of people +1. **Access API Key**: Click the key-lock button (🔒) to view your unique API key +2. **Upload Data**: Use curl or any HTTP client to upload JSON data: + ```bash + curl -X POST \ + -H "Content-Type: application/json" \ + -H "X-API-Key: YOUR_API_KEY" \ + --data @sample-data.json \ + "http://your-domain.com/api/v1/upload" + ``` +3. **Auto-reload**: The running app will detect new data and show a reload button -## Available Scripts +**API Endpoints:** +- `POST /api/v1/upload` - Upload sample data +- `GET /api/v1/sample` - Retrieve current sample data +- `GET /api/v1/state` - Get current state ID +- `GET /api/v1/health` - Simple health check (returns "OK") +- `GET /api/v1/status` - Detailed status information (JSON) -In the project directory, you can run: +## Server Configuration -### `npm start` +The server can be configured using environment variables: -Runs the app in development mode. The page will reload when you make edits. +**Network Settings:** +- `LISTEN_ADDR` - Server bind address (default: `127.0.0.1`) +- `LISTEN_PORT` - Server port (default: `3000`) -### `npm test` +**Session Management:** +- `MAX_SESSIONS` - Maximum number of concurrent sessions (default: `100`) +- `MAX_SAMPLE_SIZE` - Maximum size of uploaded sample data in bytes (default: `1048576` - 1MB) +- `MAX_SESSION_TTL` - Session time-to-live in milliseconds (default: `3600000` - 1 hour) -Launches the test runner in interactive watch mode. +Example usage: -### `npm run build` - -Builds the app for production to the `build` folder. It correctly bundles React in production mode and optimizes the build for the best performance. - -### `npm run serve` - -Serves the production build locally on port 3000. - -### Docker Scripts - -### `npm run docker:build` - -Builds a Docker container. - -### `npm run docker:run` - -Runs the Docker container. - -## Project Structure - -``` -jmespath-playground/ -├── .github/ -│ ├── workflows/ -│ │ └── build-container.yml # CI/CD pipeline -│ └── copilot-instructions.md # AI agent instructions -├── public/ -│ ├── index.html -│ ├── manifest.json -│ └── favicon.ico -├── src/ -│ ├── App.js # Main application component -│ ├── App.css # App-specific styles -│ ├── App.test.js # App tests -│ ├── index.js # React entry point -│ ├── index.css # Global styles -│ ├── setupTests.js # Test configuration -│ └── reportWebVitals.js -├── scripts/ -│ ├── build.sh # Build script -│ └── dev.sh # Development script -├── Dockerfile # Docker container -├── Dockerfile.dev # Development container -├── docker-compose.yml # Container orchestration -├── package.json # Dependencies and scripts -├── README.md # Comprehensive documentation -├── DEVELOPMENT.md # Developer guide -└── demo.sh # Demo script +```bash +MAX_SESSIONS=200 MAX_SAMPLE_SIZE=2097152 LISTEN_PORT=8080 node server.js ``` ## Technology Stack -- **React 18.2.0**: Frontend framework -- **Bootstrap 5.3.2**: CSS framework for styling -- **JMESPath 0.16.0**: JMESPath expression evaluation +- **React 18.2.0**: Frontend framework with modern hooks and components +- **Bootstrap 5.3.2**: CSS framework with dark/light theme support +- **JMESPath 0.16.0**: JMESPath expression evaluation library +- **Express.js 4.19.2**: Backend API server with session management - **Node.js 24 LTS**: Runtime environment -- **Docker**: Optional containerization - -## Contributing - -1. Fork the repository -2. Create a feature branch: `git checkout -b feature/new-feature` -3. Make your changes and commit them: `git commit -m 'Add new feature'` -4. Push to the branch: `git push origin feature/new-feature` -5. Submit a pull request +- **UUID 9.0.0**: Cryptographically secure session IDs +- **Container**: Containerization for easy deployment ## License @@ -177,7 +131,3 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## About JMESPath JMESPath is a query language for JSON. It allows you to declaratively specify how to extract elements from a JSON document. For more information about JMESPath syntax and capabilities, visit the [official JMESPath website](https://jmespath.org/). - -## Support - -If you encounter any issues or have questions, please [open an issue](../../issues) on GitHub. diff --git a/demo.sh b/demo.sh deleted file mode 100755 index 6f8a7ec..0000000 --- a/demo.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash - -# JMESPath Testing Tool - Demo Script - -echo "🚀 JMESPath Testing Tool Demo" -echo "===============================" -echo "" - -# Check if Node.js is installed -if command -v node &> /dev/null; then - echo "✅ Node.js version: $(node --version)" -else - echo "❌ Node.js not found. Please install Node.js 24 LTS or higher." - exit 1 -fi - -# Check if npm is installed -if command -v npm &> /dev/null; then - echo "✅ npm version: $(npm --version)" -else - echo "❌ npm not found. Please install npm." - exit 1 -fi - -# Check Docker -if command -v docker &> /dev/null; then - echo "✅ Docker available: $(docker --version | cut -d' ' -f3 | cut -d',' -f1)" - DOCKER_AVAILABLE=true -else - echo "⚠️ Docker not found" - DOCKER_AVAILABLE=false -fi - -echo "" -echo "📦 Installing dependencies..." -npm install - -echo "" -echo "🧪 Running tests..." -npm test -- --watchAll=false - -echo "" -echo "🔨 Building React application..." -echo " (Version will be automatically tagged as -dev since not building from git tag)" -npm run build - -echo "" -echo "🎉 Demo completed successfully!" -echo "" -echo "Available commands:" -echo "===================" -echo "" -echo "Development:" -echo " npm start - Start React development server (port 3000)" -echo " npm run server - Start Express API server only (port 3000)" -echo " npm test - Run test suite" -echo "" -echo "Production:" -echo " npm run build - Build React app for production" -echo " node server/server.js - Start integrated server with built app" -echo "" -if [ "$DOCKER_AVAILABLE" = true ]; then - echo "Docker:" - echo " docker build -t jmespath-playground ." - echo " docker run -p 3000:3000 jmespath-playground" - echo "" - echo "Docker Compose:" - echo " docker compose up --build" - echo " docker compose down" -else - echo "Docker (install Docker first):" - echo " docker build -t jmespath-playground ." - echo " docker run -p 3000:3000 jmespath-playground" - echo " docker compose up --build" -fi - -echo "" -echo "🌐 The application will be available at:" -echo " http://localhost:3000" \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..759c535 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh +set -e +exec node server.js \ No newline at end of file diff --git a/package.json b/package.json index a0da112..8df1695 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jmespath-playground", - "version": "1.2.2", + "version": "1.2.3", "description": "A React-based web application for testing JMESPath expressions against JSON data", "main": "index.js", "scripts": { @@ -9,7 +9,8 @@ "build": "react-scripts build", "test": "react-scripts test --watchAll=false", "test:watch": "react-scripts test", - "server": "node server.js" + "server": "node server.js", + "build-image": "node scripts/build-image.js" }, "engines": { "node": ">=24.0.0" diff --git a/scripts/build-image.js b/scripts/build-image.js new file mode 100644 index 0000000..48d8b89 --- /dev/null +++ b/scripts/build-image.js @@ -0,0 +1,89 @@ +#!/usr/bin/env node + +const { execSync } = require('child_process'); +const fs = require('fs'); + +function execCommand(command, description) { + try { + console.log(`${description}...`); + execSync(command, { stdio: 'inherit' }); + } catch (error) { + console.error(`Error: ${description} failed`); + process.exit(1); + } +} + +function getContainerTool() { + // Check for Docker first (primary tool) + try { + execSync('docker --version', { stdio: 'ignore' }); + return 'docker'; + } catch (error) { + // Fall back to Apple's container command + try { + execSync('container --version', { stdio: 'ignore' }); + return 'container'; + } catch (error) { + console.error('Error: No container tool found. Please install Docker or Apple Container Tools to build container images.'); + process.exit(1); + } + } +} + +function getVersion() { + try { + // Try to get version from git tag + const gitTag = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim(); + if (gitTag) { + return { version: gitTag.replace(/^v/, ''), isRelease: true }; + } + } catch (error) { + // Git command failed, ignore + } + + // Development build - use package.json version with -dev suffix + const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); + return { version: `${packageJson.version}-dev`, isRelease: false }; +} + +function main() { + const containerTool = getContainerTool(); + const { version, isRelease } = getVersion(); + + console.log(`Building ${isRelease ? 'release' : 'development'} version: ${version}`); + + // Build container image + const tags = isRelease + ? [ + `-t skoszewski/jmespath-playground:${version}`, + `-t skoszewski/jmespath-playground:latest` + ].join(' ') + : [ + `-t skoszewski/jmespath-playground:dev`, + `-t skoszewski/jmespath-playground:latest` + ].join(' '); + + const buildCommand = `${containerTool} build --build-arg VERSION="${version}" --build-arg IS_RELEASE="${isRelease}" ${tags} .`; + + execCommand(buildCommand, 'Building container image'); + + console.log('Container image build completed successfully!'); + + // Show usage instructions + if (isRelease) { + console.log(`\nTo run the container:`); + console.log(` ${containerTool} run -p 3000:3000 skoszewski/jmespath-playground:${version}`); + if (containerTool === 'docker') { + console.log(`\nTo push to Docker Hub:`); + console.log(` docker push skoszewski/jmespath-playground:${version}`); + console.log(` docker push skoszewski/jmespath-playground:latest`); + } + } else { + console.log(`\nTo run the container:`); + console.log(` ${containerTool} run -p 3000:3000 skoszewski/jmespath-playground:dev`); + } +} + +if (require.main === module) { + main(); +} \ No newline at end of file diff --git a/scripts/build-image.sh b/scripts/build-image.sh deleted file mode 100755 index 3def7a0..0000000 --- a/scripts/build-image.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -# JMESPath Testing Tool - Docker Image Build Script - -set -e - -echo "🐳 JMESPath Testing Tool - Docker Image Build" -echo "==============================================" -echo "" - -# Check if Docker is available -if ! command -v docker &> /dev/null; then - echo "❌ Docker not found. Please install Docker to build container images." - exit 1 -fi - -# Determine version information for Docker build -VERSION=$(git tag --points-at HEAD 2>/dev/null | sed 's/^v//' | head -n 1) - -if [ -n "$VERSION" ]; then - # We're at a tagged commit - release build - echo "📦 Building release version: $VERSION" - docker build \ - --build-arg VERSION="$VERSION" \ - --build-arg IS_RELEASE="true" \ - -t skoszewski/jmespath-playground:$VERSION \ - -t skoszewski/jmespath-playground:latest . - echo "✅ Built Docker images: skoszewski/jmespath-playground:$VERSION, skoszewski/jmespath-playground:latest" - - echo "" - echo "To run the release container:" - echo " docker run -p 3000:3000 skoszewski/jmespath-playground:$VERSION" - echo " docker run -p 3000:3000 skoszewski/jmespath-playground:latest" - - echo "" - echo "To push to Docker Hub:" - echo " docker push skoszewski/jmespath-playground:$VERSION" - echo " docker push skoszewski/jmespath-playground:latest" -else - # Development build - PACKAGE_VERSION=$(grep '"version"' package.json | cut -d'"' -f4) - DEV_VERSION="${PACKAGE_VERSION}-dev" - echo "📦 Building development version: $DEV_VERSION" - docker build \ - --build-arg VERSION="$DEV_VERSION" \ - --build-arg IS_RELEASE="false" \ - -t skoszewski/jmespath-playground:dev \ - -t skoszewski/jmespath-playground:latest . - echo "✅ Built Docker images: skoszewski/jmespath-playground:dev, skoszewski/jmespath-playground:latest" - - echo "" - echo "To run the development container:" - echo " docker run -p 3000:3000 skoszewski/jmespath-playground:dev" - echo " docker run -p 3000:3000 skoszewski/jmespath-playground:latest" - - echo "" - echo "To push to Docker Hub:" - echo " docker push skoszewski/jmespath-playground:dev" - echo " docker push skoszewski/jmespath-playground:latest" -fi - -echo "" -echo "🎉 Docker image build completed successfully!" \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index 617e0be..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -# JMESPath Testing Tool - Build Script - -set -e - -echo "🚀 JMESPath Testing Tool - Build Script" -echo "=======================================" -echo "" - -# Check Node.js version -if command -v node &> /dev/null; then - NODE_VERSION=$(node --version | sed 's/v//') - MAJOR_VERSION=$(echo $NODE_VERSION | cut -d. -f1) - if [ "$MAJOR_VERSION" -ge 24 ]; then - echo "✅ Node.js $NODE_VERSION (compatible with v24+ requirement)" - else - echo "❌ Node.js $NODE_VERSION found, but v24+ is required" - exit 1 - fi -else - echo "❌ Node.js not found. Please install Node.js 24 LTS or higher." - exit 1 -fi - -# Build the React application -echo "📦 Installing dependencies..." -npm install - -echo "🔨 Building production bundle..." -npm run build - -echo "✅ Build completed successfully!" -echo "" -echo "To run the application:" -echo " npm run server # Run integrated server locally" -echo "" -echo "To build Docker image:" -echo " scripts/build-image.sh # Build Docker container image" \ No newline at end of file diff --git a/scripts/dev.sh b/scripts/dev.sh deleted file mode 100755 index 579ad6c..0000000 --- a/scripts/dev.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -# JMESPath Testing Tool - Development Script - -set -e - -echo "🚀 JMESPath Testing Tool - Development" -echo "=====================================" -echo "" - -# Check Node.js -if command -v node &> /dev/null; then - echo "✅ Node.js version: $(node --version)" -else - echo "❌ Node.js not found. Please install Node.js 24 LTS." - exit 1 -fi - -# Install dependencies if needed -if [ ! -d "node_modules" ]; then - echo "📦 Installing dependencies..." - npm install -fi - -# Start development server -echo "🚀 Starting development server..." -echo " The app will open at http://localhost:3000" -echo " Press Ctrl+C to stop the server" -echo "" -npm start \ No newline at end of file diff --git a/scripts/upload.js b/scripts/upload.js index f716119..8b745d4 100755 --- a/scripts/upload.js +++ b/scripts/upload.js @@ -2,63 +2,57 @@ /** * JMESPath Playground Upload Script (JavaScript) - * Usage: node upload.js [-u URL] "json_file.json" + * Usage: node upload.js [-u URL] [-k API_KEY] "json_file.json" */ const fs = require('fs'); const path = require('path'); +const https = require('https'); +const http = require('http'); +const { URL } = require('url'); +const { parseArgs } = require('util'); function showUsage() { const scriptName = path.basename(process.argv[1]); - console.log(`Usage: node ${scriptName} [-u|--url URL] `); + console.log(`Usage: node ${scriptName} [-u|--url URL] [-k|--key API_KEY] `); console.log(''); console.log('Options:'); - console.log(' -u, --url URL API URL (default: http://localhost:3000)'); - console.log(' -h, --help Show this help message'); + console.log(' -u, --url URL API URL (default: http://localhost:3000)'); + console.log(' -k, --key API_KEY API key (not required for localhost)'); + console.log(' -h, --help Show this help message'); console.log(''); - console.log('Example:'); + console.log('Examples:'); console.log(` node ${scriptName} data.json`); - console.log(` node ${scriptName} -u http://example.com:3000 data.json`); + console.log(` node ${scriptName} -u http://example.com:3000 -k your-api-key data.json`); } -function parseArguments() { - const args = process.argv.slice(2); - let apiUrl = 'http://localhost:3000'; - let jsonFile = ''; +function getArguments() { + const { values, positionals } = parseArgs({ + args: process.argv.slice(2), + options: { + url: { type: 'string', short: 'u', default: 'http://localhost:3000' }, + key: { type: 'string', short: 'k' }, + help: { type: 'boolean', short: 'h' } + }, + allowPositionals: true + }); - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - if (arg === '-u' || arg === '--url') { - if (i + 1 >= args.length) { - console.error('Error: URL argument required for -u/--url option'); - process.exit(1); - } - apiUrl = args[i + 1]; - i++; // Skip next argument - } else if (arg === '-h' || arg === '--help') { - showUsage(); - process.exit(0); - } else if (arg.startsWith('-')) { - console.error(`Error: Unknown option ${arg}`); - showUsage(); - process.exit(1); - } else { - if (jsonFile) { - console.error('Error: Multiple JSON files specified'); - process.exit(1); - } - jsonFile = arg; - } + if (values.help) { + showUsage(); + process.exit(0); } - if (!jsonFile) { + if (positionals.length !== 1) { console.error('Error: JSON file required'); showUsage(); process.exit(1); } - return { apiUrl, jsonFile }; + return { + apiUrl: values.url, + apiKey: values.key || '', + jsonFile: positionals[0] + }; } async function validateJsonFile(jsonFile) { @@ -80,28 +74,84 @@ async function validateJsonFile(jsonFile) { } } -async function uploadData(apiUrl, jsonFile, jsonData) { - console.log('Uploading sample data to JMESPath Playground...'); - console.log(`JSON file: ${jsonFile}`); - console.log(`API URL: ${apiUrl}`); - console.log(''); - +function isLocalhost(url) { try { - const response = await fetch(`${apiUrl}/api/v1/upload`, { + const parsed = new URL(url); + const hostname = parsed.hostname; + return hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname.startsWith('127.') || + hostname === '::1'; + } catch { + return false; + } +} + +function makeRequest(url, options) { + return new Promise((resolve, reject) => { + const parsedUrl = new URL(url); + const isHttps = parsedUrl.protocol === 'https:'; + const client = isHttps ? https : http; + + const requestOptions = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.pathname, + method: options.method || 'GET', + headers: options.headers || {} + }; + + const req = client.request(requestOptions, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + resolve({ + ok: res.statusCode >= 200 && res.statusCode < 300, + status: res.statusCode, + json: () => Promise.resolve(JSON.parse(data)) + }); + }); + }); + + req.on('error', reject); + + if (options.body) { + req.write(options.body); + } + + req.end(); + }); +} + +async function uploadData(apiUrl, apiKey, jsonFile, jsonData) { + try { + const headers = { + 'Content-Type': 'application/json', + }; + + // Only send API key for non-localhost requests + const isLocal = isLocalhost(apiUrl); + if (!isLocal && apiKey) { + headers['X-API-Key'] = apiKey; + } else if (!isLocal && !apiKey) { + console.error('Error: API key required for non-localhost URLs'); + console.error('Use -k/--key option to specify API key'); + process.exit(1); + } + + const response = await makeRequest(`${apiUrl}/api/v1/upload`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, body: jsonData }); if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const errorData = await response.json().catch(() => ({})); + throw new Error(`HTTP ${response.status}: ${errorData.error || 'Upload failed'}`); } - console.log('Sample data uploaded successfully!'); - console.log(`Open ${apiUrl} in your browser to see the reload button.`); - console.log('You can then enter your JMESPath expression in the web interface.'); + const result = await response.json(); + console.log(JSON.stringify(result)); } catch (error) { console.error('Error uploading data:', error.message); @@ -110,9 +160,9 @@ async function uploadData(apiUrl, jsonFile, jsonData) { } async function main() { - const { apiUrl, jsonFile } = parseArguments(); + const { apiUrl, apiKey, jsonFile } = getArguments(); const jsonData = await validateJsonFile(jsonFile); - await uploadData(apiUrl, jsonFile, jsonData); + await uploadData(apiUrl, apiKey, jsonFile, jsonData); } // Run the script diff --git a/scripts/version-check.js b/scripts/version-check.js index 49a2ae3..d9bc7e2 100755 --- a/scripts/version-check.js +++ b/scripts/version-check.js @@ -22,15 +22,15 @@ try { console.log(`✅ Building release version ${version} (tagged: ${gitTag})`); isRelease = true; } else { - // We're not at a tagged commit - add -dev suffix - version = `${version}-dev`; - console.log(`📦 Building development version ${version}`); + // We're not at a tagged commit - use unknown version + version = 'unknown'; + console.log(`📦 Building development version with unknown version`); isRelease = false; } } catch (error) { // Git command failed (maybe not in a git repo) - version = `${version}-dev`; - console.log(`⚠️ Cannot determine git status, using development version ${version}`); + version = 'unknown'; + console.log(`⚠️ Cannot determine git status, using unknown version`); isRelease = false; } diff --git a/server.js b/server.js index fc30466..b79be5f 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,9 @@ const express = require('express'); const path = require('path'); const crypto = require('crypto'); +const os = require('os'); const { v4: uuidv4 } = require('uuid'); +const { parseArgs } = require('util'); // Environment configuration const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS) || 100; @@ -16,21 +18,21 @@ function generateSalt() { 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 || + 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; } @@ -40,12 +42,12 @@ function encrypt(data, key) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, key, iv); cipher.setAAD(Buffer.from('session-data')); - + let encrypted = cipher.update(JSON.stringify(data), 'utf8'); encrypted = Buffer.concat([encrypted, cipher.final()]); - + const authTag = cipher.getAuthTag(); - + return { iv: iv.toString('hex'), data: encrypted.toString('hex'), @@ -69,10 +71,10 @@ function decrypt(encryptedObj, key) { const decipher = crypto.createDecipheriv(algorithm, key, iv); decipher.setAAD(Buffer.from('session-data')); decipher.setAuthTag(Buffer.from(encryptedObj.tag, 'hex')); - + let decrypted = decipher.update(Buffer.from(encryptedObj.data, 'hex'), null, 'utf8'); decrypted += decipher.final('utf8'); - + return JSON.parse(decrypted); } catch (error) { console.error('⚠️ Decryption exception:', { @@ -141,7 +143,7 @@ function createApp() { // Check if request is from localhost - if so, skip API key validation const isFromLocalhost = isLocalhostRequest(req); let apiKey = req.headers['x-api-key']; - + if (!isFromLocalhost) { // Validate API key header for remote clients if (!apiKey || !isValidApiKey(apiKey)) { @@ -159,7 +161,7 @@ function createApp() { // Check session limits 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.', maxSessions: MAX_SESSIONS, currentSessions: sessions.size @@ -176,7 +178,7 @@ function createApp() { // Check data size const dataSize = Buffer.byteLength(JSON.stringify(uploadedData), 'utf8'); if (dataSize > MAX_SAMPLE_SIZE) { - return res.status(413).json({ + return res.status(413).json({ error: 'Sample data too large', maxSize: MAX_SAMPLE_SIZE, receivedSize: dataSize @@ -190,7 +192,7 @@ function createApp() { // Encrypt and store session data const encryptedData = encrypt(uploadedData, key); - + sessions.set(sessionId, { salt: salt.toString('hex'), encryptedData, @@ -201,8 +203,8 @@ function createApp() { console.log(`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`); - res.json({ - message: 'Sample data uploaded successfully', + res.json({ + message: 'Sample data uploaded successfully', state: stateGuid, sessionId: sessionId.substring(0, 8) + '...' }); @@ -213,25 +215,25 @@ function createApp() { sessionCount: sessions.size, timestamp: new Date().toISOString() }); - + // Provide more specific error messages based on error type if (error.name === 'SyntaxError') { - return res.status(400).json({ + return res.status(400).json({ error: 'Invalid JSON data format', details: 'The uploaded data could not be parsed as valid JSON' }); } else if (error.message.includes('encrypt')) { - return res.status(500).json({ + return res.status(500).json({ error: 'Encryption failed', details: 'Failed to encrypt session data. Please try again with a new API key.' }); } else if (error.message.includes('PBKDF2')) { - return res.status(500).json({ + return res.status(500).json({ error: 'Key derivation failed', details: 'Failed to derive encryption key from API key' }); } else { - return res.status(500).json({ + return res.status(500).json({ error: 'Upload processing failed', details: 'An unexpected error occurred while processing your upload. Please try again.' }); @@ -244,7 +246,7 @@ function createApp() { // Check if request is from localhost - if so, skip API key validation const isFromLocalhost = isLocalhostRequest(req); let apiKey = req.headers['x-api-key']; - + if (!isFromLocalhost) { // Validate API key header for remote clients if (!apiKey || !isValidApiKey(apiKey)) { @@ -281,25 +283,25 @@ function createApp() { sessionCount: sessions.size, timestamp: new Date().toISOString() }); - + // Provide more specific error messages based on error type if (error.message.includes('decrypt')) { - return res.status(500).json({ + return res.status(500).json({ error: 'Decryption failed', details: 'Failed to decrypt session data. The session may be corrupted or the API key may be incorrect.' }); } else if (error.message.includes('JSON')) { - return res.status(500).json({ + return res.status(500).json({ error: 'Data corruption detected', details: 'The stored session data appears to be corrupted and cannot be parsed.' }); } else if (error.name === 'TypeError') { - return res.status(500).json({ + return res.status(500).json({ error: 'Session data format error', details: 'The session data format is invalid or corrupted.' }); } else { - return res.status(500).json({ + return res.status(500).json({ error: 'Sample retrieval failed', details: 'An unexpected error occurred while retrieving sample data. The session may have been corrupted.' }); @@ -312,7 +314,7 @@ function createApp() { // Check if request is from localhost - if so, skip API key validation const isFromLocalhost = isLocalhostRequest(req); let apiKey = req.headers['x-api-key']; - + if (!isFromLocalhost) { // Validate API key header for remote clients if (!apiKey || !isValidApiKey(apiKey)) { @@ -341,15 +343,15 @@ function createApp() { sessionCount: sessions.size, timestamp: new Date().toISOString() }); - + // Provide more specific error messages if (error.message.includes('API key')) { - return res.status(403).json({ + return res.status(403).json({ error: 'API key processing failed', details: 'Failed to process the provided API key' }); } else { - return res.status(500).json({ + return res.status(500).json({ error: 'State retrieval failed', details: 'An unexpected error occurred while retrieving session state. Please try again.' }); @@ -357,9 +359,9 @@ function createApp() { } }); - // Health endpoint (no auth required) - app.get('/api/v1/health', (req, res) => { - cleanupExpiredSessions(); // Cleanup on health check + // Status endpoint (no auth required) - detailed information + app.get('/api/v1/status', (req, res) => { + cleanupExpiredSessions(); // Cleanup on status check res.json({ status: 'healthy', sessions: { @@ -376,6 +378,11 @@ function createApp() { }); }); + // Health endpoint (no auth required) - simple OK response + app.get('/api/v1/health', (req, res) => { + res.type('text/plain').send('OK'); + }); + // Serve React app for all other routes app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'build', 'index.html')); @@ -386,37 +393,62 @@ function createApp() { // Start server if this file is run directly if (require.main === module) { - // Parse command line arguments - const args = process.argv.slice(2); - let listenAddr = process.env.LISTEN_ADDR || '127.0.0.1'; - let listenPort = process.env.LISTEN_PORT || 3000; - - for (let i = 0; i < args.length; i++) { - if (args[i] === '-h' || args[i] === '--listen-addr') { - listenAddr = args[i + 1]; - i++; - } else if (args[i] === '-p' || args[i] === '--port') { - listenPort = args[i + 1]; - i++; + const { values } = parseArgs({ + options: { + 'listen-addr': { type: 'string', short: 'h', default: process.env.LISTEN_ADDR || '127.0.0.1' }, + 'port': { type: 'string', short: 'p', default: process.env.LISTEN_PORT || '3000' } } - } + }); const app = createApp(); - const PORT = parseInt(listenPort); - const HOST = listenAddr; + const PORT = parseInt(values.port); + const HOST = values['listen-addr']; app.listen(PORT, HOST, () => { - console.log(`🚀 JMESPath Playground Server running on http://${HOST}:${PORT}`); - console.log(`📊 Configuration:`); + console.log(`JMESPath Playground Server running`); + + // Show actual accessible URLs + if (HOST === '0.0.0.0') { + console.log(` Listening on all interfaces:`); + const interfaces = os.networkInterfaces(); + for (const [name, addrs] of Object.entries(interfaces)) { + for (const addr of addrs) { + if (addr.family === 'IPv4' && !addr.internal) { + console.log(` http://${addr.address}:${PORT}`); + } + } + } + // Also show localhost for local access + console.log(` http://127.0.0.1:${PORT}`); + } else { + console.log(` http://${HOST}:${PORT}`); + } + + console.log(`Configuration:`); console.log(` Max Sessions: ${MAX_SESSIONS}`); console.log(` 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(`🔗 API endpoints:`); - console.log(` POST http://${HOST}:${PORT}/api/v1/upload (requires X-API-Key)`); - console.log(` GET http://${HOST}:${PORT}/api/v1/sample (requires X-API-Key)`); - console.log(` GET http://${HOST}:${PORT}/api/v1/state (requires X-API-Key)`); - console.log(` GET http://${HOST}:${PORT}/api/v1/health (public)`); - console.log(`🔐 Security: AES-256-GCM encryption with PBKDF2 key derivation`); + + // Show base API URL + let apiBaseUrl; + if (HOST === '0.0.0.0') { + const interfaces = os.networkInterfaces(); + let firstIP = '127.0.0.1'; + outer: for (const addrs of Object.values(interfaces)) { + for (const addr of addrs) { + if (addr.family === 'IPv4' && !addr.internal) { + firstIP = addr.address; + break outer; + } + } + } + apiBaseUrl = `http://${firstIP}:${PORT}/api/v1`; + } else { + apiBaseUrl = `http://${HOST}:${PORT}/api/v1`; + } + + console.log(`API Base URL: ${apiBaseUrl}`); + console.log(`Security: AES-256-GCM encryption with PBKDF2 key derivation`); }); } diff --git a/src/App.test.js b/src/App.test.js index 6b622ab..ff486a9 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -55,16 +55,19 @@ describe('App Component', () => { test('renders version number', () => { render(); - const versionText = screen.getByText(/v\d+\.\d+\.\d+(-dev|-test)?/); + // Version can be either v1.2.3 format (release), v1.2.3-dev/test format (legacy dev), or "unknown" format (new dev) + const versionText = screen.getByText(/(v\d+\.\d+\.\d+(-dev|-test)?|unknown)/); expect(versionText).toBeInTheDocument(); - // Check if it's a dev/test build - const isDevBuild = versionText.textContent.includes('-dev') || versionText.textContent.includes('-test'); + // Check if it's a dev/test/unknown build + const isDevBuild = versionText.textContent.includes('-dev') || + versionText.textContent.includes('-test') || + versionText.textContent.includes('unknown'); // Additional validations can be added here based on build type if (isDevBuild) { - // Dev/test specific validations could go here - expect(versionText.textContent).toMatch(/v\d+\.\d+\.\d+-(dev|test)/); + // Dev/test/unknown specific validations + expect(versionText.textContent).toMatch(/(v\d+\.\d+\.\d+-(dev|test)|unknown)/); } else { // Release build validations - just check that version pattern exists in the text expect(versionText.textContent).toMatch(/v\d+\.\d+\.\d+/); diff --git a/src/components/Footer.js b/src/components/Footer.js index 45760af..087b831 100644 --- a/src/components/Footer.js +++ b/src/components/Footer.js @@ -8,7 +8,7 @@ function Footer() {

- JMESPath Testing Tool v{VERSION} - Created for testing and validating JMESPath expressions + JMESPath Testing Tool {VERSION === 'unknown' ? VERSION : `v${VERSION}`} - Created for testing and validating JMESPath expressions