diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..1e6d473 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,30 @@ +{ + "permissions": { + "allow": [ + "Bash(npm:*)", + "Bash(yarn:*)", + "Bash(npx:*)", + "Bash(node:*)", + "Bash(curl:*)", + "Bash(gh:*)", + "Bash(jq:*)", + "Bash(mkdir:*)", + "Bash(docker:*)", + "Bash(docker compose:*)", + "Edit", + "Read", + "Write", + "Grep", + "Glob", + "LS" + ], + "deny": [ + "Read(./.env)", + "Read(./.env.*)", + "Read(./secrets/**)" + ], + "ask": [ + "Bash(git push:*)" + ] + } +} diff --git a/.claude/skills/run-tests/SKILL.md b/.claude/skills/run-tests/SKILL.md new file mode 100644 index 0000000..0b3b60d --- /dev/null +++ b/.claude/skills/run-tests/SKILL.md @@ -0,0 +1,195 @@ +--- +name: run-tests +description: Run API tests against InfluxDB 3 Core. Handles service initialization, database setup, and test execution. +author: InfluxData +version: "1.0" +--- + +# Run Tests Skill + +## Purpose + +This skill guides running the IoT API test suite against a local InfluxDB 3 Core instance. It covers service setup, database creation, and test execution. + +## Quick Reference + +| Task | Command | +|------|---------| +| Start InfluxDB 3 | `docker compose up -d influxdb3-core` | +| Check status | `curl -i http://localhost:8181/health` | +| Run tests | `yarn test` | +| View logs | `docker logs influxdb3-core` | + +## Complete Setup Workflow + +### 1. Initialize InfluxDB 3 Core + +```bash +# Create required directories +mkdir -p test/.influxdb3/core/data test/.influxdb3/core/plugins + +# Generate admin token (first time only) +openssl rand -hex 32 > test/.influxdb3/core/.token +chmod 600 test/.influxdb3/core/.token + +# Start the container +docker compose up -d influxdb3-core + +# Wait for healthy status +docker compose ps +``` + +### 2. Create Databases + +```bash +# Get the token +TOKEN=$(cat test/.influxdb3/core/.token) + +# Create main database +curl -X POST "http://localhost:8181/api/v3/configure/database" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"db": "iot_center"}' + +# Create auth database +curl -X POST "http://localhost:8181/api/v3/configure/database" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"db": "iot_center_auth"}' + +# Verify databases exist +curl "http://localhost:8181/api/v3/configure/database?format=json" \ + -H "Authorization: Bearer $TOKEN" +``` + +### 3. Configure Environment + +Create `.env.local` if it doesn't exist: + +```bash +cat > .env.local << EOF +INFLUX_HOST=http://localhost:8181 +INFLUX_TOKEN=$(cat test/.influxdb3/core/.token) +INFLUX_DATABASE=iot_center +INFLUX_DATABASE_AUTH=iot_center_auth +EOF +``` + +### 4. Run Tests + +```bash +# Install dependencies (if needed) +yarn + +# Run the test suite +yarn test + +# Run with verbose output +yarn test --verbose + +# Run specific test file +yarn test __tests__/api.test.js +``` + +## Troubleshooting + +### Container Won't Start + +**Symptom:** Container exits immediately + +**Check:** +```bash +# View logs +docker logs influxdb3-core + +# Verify directories exist +ls -la test/.influxdb3/core/ + +# Verify token file exists +cat test/.influxdb3/core/.token +``` + +**Common fixes:** +- Create missing directories: `mkdir -p test/.influxdb3/core/data test/.influxdb3/core/plugins` +- Generate token: `openssl rand -hex 32 > test/.influxdb3/core/.token` + +### 401 Unauthorized + +**Symptom:** API calls return 401 + +**Check:** +```bash +# Verify token matches +echo "Token in file: $(cat test/.influxdb3/core/.token)" +echo "Token in .env.local: $(grep INFLUX_TOKEN .env.local)" + +# Test with token directly +curl -i http://localhost:8181/api/v3/configure/database \ + -H "Authorization: Bearer $(cat test/.influxdb3/core/.token)" +``` + +### Database Not Found + +**Symptom:** Tests fail with "database not found" + +**Fix:** Create the required databases: +```bash +TOKEN=$(cat test/.influxdb3/core/.token) +curl -X POST "http://localhost:8181/api/v3/configure/database" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"db": "iot_center"}' +curl -X POST "http://localhost:8181/api/v3/configure/database" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"db": "iot_center_auth"}' +``` + +### Port Already in Use + +**Symptom:** "port is already allocated" + +**Fix:** +```bash +# Find what's using the port +lsof -i :8181 + +# Stop existing container +docker compose down +``` + +## Clean Slate + +To start fresh: + +```bash +# Stop and remove containers +docker compose down + +# Remove data (WARNING: deletes all data) +rm -rf test/.influxdb3/core/data/* + +# Regenerate token +openssl rand -hex 32 > test/.influxdb3/core/.token + +# Start fresh +docker compose up -d influxdb3-core +``` + +## Test Configuration + +The test suite uses these environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `INFLUX_HOST` | `http://localhost:8181` | InfluxDB 3 API URL | +| `INFLUX_TOKEN` | (from `.env.local`) | Admin token | +| `INFLUX_DATABASE` | `iot_center` | Main data database | +| `INFLUX_DATABASE_AUTH` | `iot_center_auth` | Device auth database | + +## Related Files + +- **Docker Compose**: `compose.yaml` +- **Test suite**: `__tests__/api.test.js` +- **Environment defaults**: `.env.development` +- **Local overrides**: `.env.local` (gitignored) diff --git a/.env.development b/.env.development index 3846cd4..4a6a9da 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,12 @@ # Development environment non-secret defaults +# InfluxDB 3 Core configuration -INFLUX_URL=http://localhost:8086 -INFLUX_BUCKET=iot_center -INFLUX_BUCKET_AUTH=iot_center_devices \ No newline at end of file +# InfluxDB 3 server URL (default port is 8181) +INFLUX_HOST=http://localhost:8181 + +# Database names (equivalent to v2 buckets) +INFLUX_DATABASE=iot_center +INFLUX_DATABASE_AUTH=iot_center_devices + +# Token should be set in .env.local (not committed to git) +# INFLUX_TOKEN=your_admin_token_here \ No newline at end of file diff --git a/.github/INSTRUCTIONS.md b/.github/INSTRUCTIONS.md new file mode 100644 index 0000000..a99b0d6 --- /dev/null +++ b/.github/INSTRUCTIONS.md @@ -0,0 +1,52 @@ +# AI Instructions Navigation Guide + +This repository has multiple instruction files for different AI tools and use cases. + +## Quick Navigation + +| If you are... | Use this file | Purpose | +|---------------|---------------|---------| +| **GitHub Copilot** | [../AGENTS.md](../AGENTS.md) | Development patterns, code examples | +| **Claude, ChatGPT, Gemini** | [../AGENTS.md](../AGENTS.md) | Comprehensive development guide | +| **Claude with MCP** | [../CLAUDE.md](../CLAUDE.md) | Quick reference with skill pointers | + +## File Organization + +``` +iot-api-js/ +├── .claude/ +│ ├── settings.json # Claude permissions +│ └── skills/ +│ └── run-tests/SKILL.md # Test execution workflow +├── .github/ +│ └── INSTRUCTIONS.md # THIS FILE - Navigation guide +├── AGENTS.md # Comprehensive AI assistant guide +├── CLAUDE.md # Claude with MCP quick reference +└── README.md # User-facing documentation +``` + +## What's in Each File + +**[../CLAUDE.md](../CLAUDE.md)** - Quick reference for Claude: +- Project overview +- Quick commands +- Structure summary +- Links to skills + +**[../AGENTS.md](../AGENTS.md)** - Comprehensive guide: +- Architecture diagram +- Development workflow +- Code patterns and examples +- Common tasks +- Style guidelines + +**[../README.md](../README.md)** - User documentation: +- Setup instructions +- API usage +- Troubleshooting + +## Getting Started + +1. **New to the repository?** Start with [../README.md](../README.md) +2. **Using AI assistants?** Read [../AGENTS.md](../AGENTS.md) +3. **Using Claude with MCP?** Check [../CLAUDE.md](../CLAUDE.md) and [../.claude/](../.claude/) diff --git a/.gitignore b/.gitignore index 20fccdd..c0a1c0c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ yarn-error.log* .env.development.local .env.test.local .env.production.local + +# InfluxDB 3 local data +test/.influxdb3/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..112d77b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,175 @@ +# IoT API JS - AI Assistant Guide + +> **For general AI assistants (Claude, ChatGPT, Gemini, etc.)** +> +> This guide provides instructions for AI assistants helping with the IoT API sample application. +> +> **Other instruction resources**: +> - [CLAUDE.md](CLAUDE.md) - For Claude with MCP +> - [.github/INSTRUCTIONS.md](.github/INSTRUCTIONS.md) - Navigation guide +> - [README.md](README.md) - User-facing documentation + +## Project Purpose + +This is a **sample application** for InfluxData documentation tutorials. It demonstrates: +- Building REST APIs that interact with InfluxDB 3 +- Device registration with application-level authentication +- Writing and querying time-series IoT data +- Using the `@influxdata/influxdb3-client` JavaScript library + +**Target audience**: Developers learning to build IoT applications with InfluxDB 3. + +## Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Frontend UI │────▶│ Next.js API │────▶│ InfluxDB 3 │ +│ (iot-api-ui) │ │ (this repo) │ │ Core │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Two databases: │ + │ - iot_center │ + │ - iot_center │ + │ - iot_center_devices│ + └─────────────────────┘ +``` + +## Key Files + +| File | Purpose | +|------|---------| +| `lib/influxdb.js` | InfluxDB client factory and helpers | +| `pages/api/devices/create.js` | Device registration endpoint | +| `pages/api/devices/_devices.js` | Shared device query logic | +| `pages/api/devices/[[...deviceParams]].js` | Device CRUD operations | +| `pages/api/measurements/index.js` | Telemetry query endpoint | +| `__tests__/api.test.js` | API integration tests | + +## Development Workflow + +### Prerequisites + +1. Node.js 18+ and Yarn +2. Docker (for InfluxDB 3 Core) +3. Environment variables in `.env.local` + +### Local Development + +```bash +# 1. Start InfluxDB 3 Core +docker compose up -d influxdb3-core + +# 2. Get the generated token +cat test/.influxdb3/core/.token + +# 3. Configure .env.local with the token +echo "INFLUX_TOKEN=$(cat test/.influxdb3/core/.token)" >> .env.local + +# 4. Install dependencies and start +yarn +yarn dev -p 5200 + +# 5. Test the API +curl http://localhost:5200/api/devices +``` + +### Running Tests + +```bash +# Ensure InfluxDB 3 is running +docker compose up -d influxdb3-core + +# Run test suite +yarn test +``` + +## Code Patterns + +Use SQL or InfluxQL to query data. + +### SQL Queries (InfluxDB 3) + +Query InfluxDB 3 using SQL: + +```javascript +// Query devices +const sql = ` + SELECT time, deviceId, key + FROM deviceauth + WHERE deviceId = '${escapeString(deviceId)}' + ORDER BY time DESC + LIMIT 1 +` +const rows = await query(sql, database) +``` + +### Writing Data with Points + +```javascript +import { Point } from '@influxdata/influxdb3-client' + +const point = Point.measurement('deviceauth') + .setTag('deviceId', deviceId) + .setStringField('key', deviceKey) + .setStringField('token', deviceToken) + +await write(point.toLineProtocol(), database) +``` + +### Client Lifecycle + +Always close clients after use: + +```javascript +const client = createClient() +try { + // Use client... +} finally { + await client.close() +} +``` + +## Common Tasks + +### Adding a New Endpoint + +1. Create file in `pages/api/` following Next.js conventions +2. Import helpers from `lib/influxdb.js` +3. Add input validation (see `DEVICE_ID_PATTERN` example) +4. Add tests in `__tests__/` + +### Modifying Database Schema + +Data is stored as time-series measurements: +- `deviceauth` - Device registration (tags: deviceId; fields: key, token) +- Custom measurements for telemetry + +### Debugging + +```bash +# Check InfluxDB 3 logs +docker logs influxdb3-core + +# Query directly with curl +curl -X POST "http://localhost:8181/api/v3/query_sql" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"db": "iot_center", "q": "SELECT * FROM deviceauth"}' +``` + +## Style Guidelines + +- Use ES modules (`import`/`export`) +- Validate all user input before database operations +- Never expose tokens in API responses +- Use descriptive error messages +- Follow existing patterns in the codebase + +## Related Resources + +- [InfluxDB 3 Core Documentation](https://docs.influxdata.com/influxdb3/core/) +- [influxdb3-javascript Client](https://github.com/InfluxCommunity/influxdb3-js) +- [IoT Starter Tutorial](https://docs.influxdata.com/influxdb/v2/api-guide/tutorials/nodejs/) +- [iot-api-ui Frontend](https://github.com/influxdata/iot-api-ui) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2c3461c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,100 @@ +# IoT API JS - Claude Instructions + +> **For Claude with MCP** +> +> This is a Next.js REST API server demonstrating InfluxDB 3 integration for IoT device data. +> +> **Instruction resources**: +> - [AGENTS.md](AGENTS.md) - For general AI assistants +> - [.github/INSTRUCTIONS.md](.github/INSTRUCTIONS.md) - Navigation guide +> - [.claude/](.claude/) - Claude configuration (settings, skills) + +## Project Overview + +This sample application demonstrates how to build an IoT data collection API using: +- **Next.js** - React framework for the API server +- **InfluxDB 3 Core** - Time-series database +- **@influxdata/influxdb3-client** - Official JavaScript client + +## Quick Commands + +| Task | Command | +|------|---------| +| Install dependencies | `yarn` | +| Start dev server | `yarn dev -p 5200` | +| Run tests | `yarn test` | +| Start InfluxDB 3 | `docker compose up -d influxdb3-core` | + +## Project Structure + +``` +iot-api-js/ +├── lib/ +│ └── influxdb.js # InfluxDB client helpers +├── pages/api/ +│ ├── devices/ # Device CRUD endpoints +│ │ ├── create.js # POST /api/devices/create +│ │ ├── _devices.js # Shared device queries +│ │ └── [[...deviceParams]].js # GET/DELETE /api/devices +│ └── measurements/ # Telemetry endpoints +│ └── index.js # GET /api/measurements +├── __tests__/ # API tests +├── compose.yaml # InfluxDB 3 Core container +└── .env.development # Default config (committed) +``` + +## Environment Configuration + +Copy `.env.development` values to `.env.local` and customize: + +```bash +INFLUX_HOST=http://localhost:8181 +INFLUX_TOKEN=your-token +INFLUX_DATABASE=iot_center +INFLUX_DATABASE_AUTH=iot_center_devices +``` + +## Testing + +Run the test suite against a local InfluxDB 3 Core instance: + +```bash +# Start InfluxDB 3 Core +docker compose up -d influxdb3-core + +# Run tests +yarn test +``` + +See `.claude/skills/run-tests/SKILL.md` for detailed testing workflow. + +## API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/devices` | GET | List all devices | +| `/api/devices/:id` | GET | Get device by ID | +| `/api/devices/create` | POST | Create new device | +| `/api/devices/:id` | DELETE | Delete device | +| `/api/measurements` | GET | Query device telemetry | + +## Key Patterns + +### InfluxDB 3 Client Usage + +```javascript +import { createClient, query, write } from '../../../lib/influxdb' + +// Query with SQL +const results = await query('SELECT * FROM deviceauth', database) + +// Write line protocol +await write('measurement,tag=value field=1', database) +``` + +### Application-Level Tokens + +This app uses application-managed tokens stored in the database. Tokens are: +- Generated via `generateDeviceToken()` in `lib/influxdb.js` +- Stored in the `deviceauth` measurement +- Never exposed via public API responses diff --git a/README.md b/README.md index 0df96a6..96434f6 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,62 @@ # iot-api-js -This example project provides a Node.js REST API server that interacts with the InfluxDB v2 HTTP API. -The project uses the [Next.js](https://nextjs.org/) framework and the [InfluxDB v2 API client library for JavaScript](https://docs.influxdata.com/influxdb/v2/api-guide/client-libraries/nodejs/) to demonstrate how to build an app that collects, stores, and queries IoT device data. +> [!WARNING] +> #### ⚠️ Sample Application Notice +> This is a **reference implementation** for learning purposes. It demonstrates InfluxDB 3 client library usage patterns but is **not production-ready**. +> +> **Not included:** Authentication/authorization, rate limiting, comprehensive error handling, input sanitization for all edge cases, secure credential management, high availability, or production logging. +> +> **Before deploying to production**, implement proper security controls, follow your organization's security guidelines, and conduct a thorough security review. + +This example project provides a Node.js REST API server that interacts with InfluxDB 3 Core. +The project uses the [Next.js](https://nextjs.org/) framework and the [InfluxDB 3 JavaScript client library](https://github.com/InfluxCommunity/influxdb3-js) to demonstrate how to build an app that collects, stores, and queries IoT device data. After you have set up and run your `iot-api-js` API server, you can consume your API using the [iot-api-ui](https://github.com/influxdata/iot-api-ui) standalone React frontend. ## Features -This application demonstrates how you can use InfluxDB client libraries to do the following: +This application demonstrates how you can use the InfluxDB 3 client library to do the following: -- Create and manage InfluxDB authorizations (API tokens and permissions). -- Write and query device metadata in InfluxDB. -- Write and query telemetry data in InfluxDB. -- Generate data visualizations with the InfluxDB Giraffe library. +- Register IoT devices with application-level tokens. +- Write and query device metadata in InfluxDB 3. +- Write and query telemetry data using SQL queries. +- Explore data using the InfluxDB 3 SQL query interface. ## Tutorial and support -To learn how to build this app from scratch, follow the [InfluxDB v2 OSS tutorial](https://docs.influxdata.com/influxdb/v2/api-guide/tutorials/nodejs/) or [InfluxDB Cloud tutorial](https://docs.influxdata.com/influxdb/cloud/api-guide/tutorials/nodejs/). -The app is an adaptation of [InfluxData IoT Center](https://github.com/bonitoo-io/iot-center-v2), simplified to accompany the IoT Starter tutorial. +This app is an adaptation of [InfluxData IoT Center](https://github.com/bonitoo-io/iot-center-v2), simplified to demonstrate InfluxDB 3 Core integration patterns. -For help, refer to the tutorials and InfluxDB documentation or use the following resources: +For help, refer to the InfluxDB 3 documentation or use the following resources: +- [InfluxDB 3 Core Documentation](https://docs.influxdata.com/influxdb3/core/) +- [InfluxDB 3 JavaScript Client](https://github.com/InfluxCommunity/influxdb3-js) - [InfluxData Community](https://community.influxdata.com/) - [InfluxDB Community Slack](https://influxdata.com/slack) -To report a problem, submit an issue to this repo or to the [`influxdata/docs-v2` repo](https://github.com/influxdata/docs-v2/issues). +To report a problem, submit an issue to this repo. ## Get started -### Set up InfluxDB prerequisites +### Set up InfluxDB 3 Core -Follow the tutorial instructions to setup your InfluxDB organization, API token, and buckets: +1. Install and start InfluxDB 3 Core following the [installation guide](https://docs.influxdata.com/influxdb3/core/get-started/setup/). -- [Set up InfluxDB OSS v2 prerequisites](https://docs.influxdata.com/influxdb/v2/api-guide/tutorials/nodejs/#set-up-influxdb) -- [Set up InfluxDB Cloud v2 prerequisites](https://docs.influxdata.com/influxdb/cloud/api-guide/tutorials/nodejs/#set-up-influxdb) +2. Create the required databases: + + ```bash + # Create database for telemetry data + influxdb3 create database iot_center + + # Create database for device authentication + influxdb3 create database iot_center_devices + ``` + +3. Create an admin token for the API server: + + ```bash + influxdb3 create token --admin + ``` + + Save the token value for the next step. Next, [clone and run the API server](#clone-and-run-the-api-server). @@ -50,16 +74,19 @@ Next, [clone and run the API server](#clone-and-run-the-api-server). ```bash # Local environment secrets - INFLUX_TOKEN=INFLUXDB_ALL_ACCESS_TOKEN - INFLUX_ORG=INFLUXDB_ORG_ID + INFLUX_TOKEN=YOUR_ADMIN_TOKEN ``` - Replace the following: + Replace **`YOUR_ADMIN_TOKEN`** with the admin token you created in the previous step. - - **`INFLUXDB_ALL_ACCESS_TOKEN`** with your InfluxDB **All Access** token. - - **`INFLUXDB_ORG_ID`** with your InfluxDB organization ID. +4. If you need to adjust the default host or database names, edit the settings in `.env.development` or set them in `.env.local` (to override `.env.development`): -4. If you need to adjust the default URL or bucket names to match your InfluxDB instance, edit the settings in `.env.development` or set them in `.env.local` (to override `.env.development`). + ```bash + # Default settings (can be overridden in .env.local) + INFLUX_HOST=http://localhost:8181 + INFLUX_DATABASE=iot_center + INFLUX_DATABASE_AUTH=iot_center_devices + ``` 5. If you haven't already, follow the [Node.js installation instructions](https://nodejs.org/) to install `node` for your operating system. 6. To check the installed `node` version, enter the following command in your terminal: @@ -100,23 +127,26 @@ Next, [clone and run the API server](#clone-and-run-the-api-server). ## Troubleshoot -### Error: could not find bucket +### Error: could not find database ```json -{"error":"failed to load data: HttpError: failed to initialize execute state: could not find bucket \"iot_center_devices\""} +{"error":"failed to load data: database \"iot_center_devices\" not found"} ``` -Solution: [create buckets](#set-up-influxdb-prerequisites) or adjust the defaults in `.env.development` to match your InfluxDB instance. +Solution: [create the databases](#set-up-influxdb-3-core) or adjust the defaults in `.env.development` to match your InfluxDB instance. ## Learn More -### InfluxDB +### InfluxDB 3 -- Develop with the InfluxDB API for [OSS v2](https://docs.influxdata.com/influxdb/v2/api-guide/) or [Cloud v2](https://docs.influxdata.com/influxdb/cloud/api-guide/). +- [InfluxDB 3 Core Documentation](https://docs.influxdata.com/influxdb3/core/) +- [InfluxDB 3 Enterprise Documentation](https://docs.influxdata.com/influxdb3/enterprise/) +- [InfluxDB 3 JavaScript Client](https://github.com/InfluxCommunity/influxdb3-js) +- [Query data with SQL](https://docs.influxdata.com/influxdb3/core/query-data/sql/) ### Next.js -To learn more about Next.js, see following resources: +To learn more about Next.js, see the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. diff --git a/__tests__/api.test.js b/__tests__/api.test.js new file mode 100644 index 0000000..2a25aca --- /dev/null +++ b/__tests__/api.test.js @@ -0,0 +1,283 @@ +/** + * API Endpoint Tests for iot-api-js + * + * These tests verify the behavior of the IoT API endpoints. + * Run with: npm test (after adding test script to package.json) + * + * Note: These tests mock the InfluxDB client to run without a database. + */ + +import { createMocks } from 'node-mocks-http' + +// Mock the influxdb module before importing handlers +jest.mock('../lib/influxdb', () => ({ + query: jest.fn(), + write: jest.fn(), + config: { + host: 'http://localhost:8181', + token: 'test-token', + database: 'iot_center', + databaseAuth: 'iot_center_devices', + }, + generateDeviceToken: jest.fn(() => 'iot_mock_token_12345'), + Point: { + measurement: jest.fn(() => ({ + setTag: jest.fn().mockReturnThis(), + setStringField: jest.fn().mockReturnThis(), + toLineProtocol: jest.fn(() => 'deviceauth,deviceId=test-device key="test-key",token="test-token"'), + })), + }, +})) + +import createHandler from '../pages/api/devices/create' +import devicesHandler from '../pages/api/devices/[[...deviceParams]]' +import { query, write } from '../lib/influxdb' + +describe('POST /api/devices/create', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('creates a new device with valid deviceId', async () => { + query.mockResolvedValue([]) // No existing device + write.mockResolvedValue(undefined) + + const { req, res } = createMocks({ + method: 'POST', + body: { deviceId: 'sensor-001' }, + }) + + await createHandler(req, res) + + expect(res._getStatusCode()).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.deviceId).toBe('sensor-001') + expect(data.token).toBeDefined() + expect(data.message).toContain('registered successfully') + }) + + test('rejects invalid deviceId with special characters', async () => { + const { req, res } = createMocks({ + method: 'POST', + body: { deviceId: 'sensor' }, + }) + + await createHandler(req, res) + + expect(res._getStatusCode()).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toContain('Invalid deviceId format') + }) + + test('rejects deviceId with newlines (injection attempt)', async () => { + const { req, res } = createMocks({ + method: 'POST', + body: { deviceId: 'sensor\nmalicious,tag=evil field=1' }, + }) + + await createHandler(req, res) + + expect(res._getStatusCode()).toBe(400) + }) + + test('rejects missing deviceId', async () => { + const { req, res } = createMocks({ + method: 'POST', + body: {}, + }) + + await createHandler(req, res) + + expect(res._getStatusCode()).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toContain('deviceId is required') + }) + + test('rejects non-POST methods', async () => { + const { req, res } = createMocks({ + method: 'GET', + }) + + await createHandler(req, res) + + expect(res._getStatusCode()).toBe(405) + }) + + test('rejects duplicate device registration', async () => { + query.mockResolvedValue([ + { deviceId: 'existing-device', key: 'existing-key', time: new Date() }, + ]) + + const { req, res } = createMocks({ + method: 'POST', + body: { deviceId: 'existing-device' }, + }) + + await createHandler(req, res) + + expect(res._getStatusCode()).toBe(500) + const data = JSON.parse(res._getData()) + expect(data.error).toContain('already registered') + }) +}) + +describe('GET /api/devices', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('lists all devices without tokens', async () => { + query.mockResolvedValue([ + { deviceId: 'device-1', key: 'key-1', time: new Date() }, + { deviceId: 'device-2', key: 'key-2', time: new Date() }, + ]) + + const { req, res } = createMocks({ + method: 'GET', + query: { deviceParams: [] }, + }) + + await devicesHandler(req, res) + + expect(res._getStatusCode()).toBe(200) + const data = JSON.parse(res._getData()) + expect(data).toHaveLength(2) + // Verify tokens are NOT exposed + expect(data[0].token).toBeUndefined() + expect(data[1].token).toBeUndefined() + }) + + test('returns specific device without token', async () => { + query.mockResolvedValue([ + { deviceId: 'device-1', key: 'key-1', time: new Date() }, + ]) + + const { req, res } = createMocks({ + method: 'GET', + query: { deviceParams: ['device-1'] }, + }) + + await devicesHandler(req, res) + + expect(res._getStatusCode()).toBe(200) + const data = JSON.parse(res._getData()) + expect(data[0].deviceId).toBe('device-1') + // Token should NOT be exposed even for specific device + expect(data[0].token).toBeUndefined() + }) +}) + +describe('POST /api/devices/:deviceId/measurements', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('executes valid SELECT query', async () => { + query.mockResolvedValue([ + { time: new Date(), room: 'Kitchen', temp: 22.5 }, + ]) + + const { req, res } = createMocks({ + method: 'POST', + query: { deviceParams: ['device-1', 'measurements'] }, + body: { query: 'SELECT * FROM home LIMIT 10' }, + }) + + await devicesHandler(req, res) + + expect(res._getStatusCode()).toBe(200) + }) + + test('rejects DROP TABLE query', async () => { + const { req, res } = createMocks({ + method: 'POST', + query: { deviceParams: ['device-1', 'measurements'] }, + body: { query: 'DROP TABLE home' }, + }) + + await devicesHandler(req, res) + + expect(res._getStatusCode()).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toContain('Only SELECT queries are allowed') + }) + + test('rejects DELETE query', async () => { + const { req, res } = createMocks({ + method: 'POST', + query: { deviceParams: ['device-1', 'measurements'] }, + body: { query: 'DELETE FROM home WHERE 1=1' }, + }) + + await devicesHandler(req, res) + + expect(res._getStatusCode()).toBe(400) + }) + + test('rejects UPDATE query', async () => { + const { req, res } = createMocks({ + method: 'POST', + query: { deviceParams: ['device-1', 'measurements'] }, + body: { query: "UPDATE home SET temp=0 WHERE room='Kitchen'" }, + }) + + await devicesHandler(req, res) + + expect(res._getStatusCode()).toBe(400) + }) + + test('rejects multi-statement injection', async () => { + const { req, res } = createMocks({ + method: 'POST', + query: { deviceParams: ['device-1', 'measurements'] }, + body: { query: 'SELECT * FROM home; DROP TABLE home' }, + }) + + await devicesHandler(req, res) + + expect(res._getStatusCode()).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toContain('blocked operations') + }) + + test('rejects excessively long queries', async () => { + const longQuery = 'SELECT * FROM home WHERE ' + 'x=1 OR '.repeat(500) + + const { req, res } = createMocks({ + method: 'POST', + query: { deviceParams: ['device-1', 'measurements'] }, + body: { query: longQuery }, + }) + + await devicesHandler(req, res) + + expect(res._getStatusCode()).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toContain('maximum length') + }) + + test('rejects missing query parameter', async () => { + const { req, res } = createMocks({ + method: 'POST', + query: { deviceParams: ['device-1', 'measurements'] }, + body: {}, + }) + + await devicesHandler(req, res) + + expect(res._getStatusCode()).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toContain('Missing query parameter') + }) + + test('rejects GET method for measurements', async () => { + const { req, res } = createMocks({ + method: 'GET', + query: { deviceParams: ['device-1', 'measurements'] }, + }) + + await devicesHandler(req, res) + + expect(res._getStatusCode()).toBe(405) + }) +}) diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..a311e08 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,74 @@ +# Docker Compose for IoT API JS +# Provides InfluxDB 3 Core for local development and testing. +name: iot-api-js + +secrets: + influxdb3-core-token: + file: test/.influxdb3/core/.token + +services: + # ============================================================================ + # InfluxDB 3 Core + # ============================================================================ + # Local development instance with file-based storage. + # + # USAGE: + # docker compose up -d influxdb3-core + # + # FIRST-TIME SETUP: + # 1. Create directories: + # mkdir -p test/.influxdb3/core/data test/.influxdb3/core/plugins + # + # 2. Generate token: + # openssl rand -hex 32 > test/.influxdb3/core/.token + # chmod 600 test/.influxdb3/core/.token + # + # 3. Start service: + # docker compose up -d influxdb3-core + # + # 4. Create databases: + # TOKEN=$(cat test/.influxdb3/core/.token) + # curl -X POST "http://localhost:8181/api/v3/configure/database" \ + # -H "Authorization: Bearer $TOKEN" \ + # -H "Content-Type: application/json" \ + # -d '{"db": "iot_center"}' + # curl -X POST "http://localhost:8181/api/v3/configure/database" \ + # -H "Authorization: Bearer $TOKEN" \ + # -H "Content-Type: application/json" \ + # -d '{"db": "iot_center_devices"}' + # + # ENDPOINTS: + # - API: http://localhost:8181 + # - Health: http://localhost:8181/health + # - Ping: http://localhost:8181/ping + # ============================================================================ + influxdb3-core: + container_name: influxdb3-core + image: influxdb:3-core + pull_policy: always + ports: + - 8181:8181 + command: + - influxdb3 + - serve + - --node-id=node0 + - --object-store=file + - --data-dir=/var/lib/influxdb3/data + - --plugin-dir=/var/lib/influxdb3/plugins + - --admin-token-file=/run/secrets/influxdb3-core-token + - --log-filter=info + volumes: + - type: bind + source: test/.influxdb3/core/data + target: /var/lib/influxdb3/data + - type: bind + source: test/.influxdb3/core/plugins + target: /var/lib/influxdb3/plugins + secrets: + - influxdb3-core-token + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8181/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s diff --git a/lib/influxdb.js b/lib/influxdb.js new file mode 100644 index 0000000..460d406 --- /dev/null +++ b/lib/influxdb.js @@ -0,0 +1,84 @@ +import { InfluxDBClient, Point } from '@influxdata/influxdb3-client' +import { randomBytes } from 'crypto' + +// Re-export Point class for use in other modules +export { Point } + +/** + * Creates a new InfluxDB 3 client instance. + * Always call client.close() when done to release resources. + * + * @returns {InfluxDBClient} A new InfluxDB client instance + */ +export function createClient() { + return new InfluxDBClient({ + host: process.env.INFLUX_HOST, + token: process.env.INFLUX_TOKEN, + }) +} + +/** + * Environment configuration for InfluxDB 3 + */ +export const config = { + get host() { + return process.env.INFLUX_HOST + }, + get token() { + return process.env.INFLUX_TOKEN + }, + get database() { + return process.env.INFLUX_DATABASE + }, + get databaseAuth() { + return process.env.INFLUX_DATABASE_AUTH + }, +} + +/** + * Executes a SQL query and returns results as an array. + * + * @param {string} sql - The SQL query to execute + * @param {string} database - The database to query + * @returns {Promise} Array of row objects + */ +export async function query(sql, database) { + const client = createClient() + try { + const results = [] + for await (const row of client.query(sql, database)) { + results.push(row) + } + return results + } finally { + await client.close() + } +} + +/** + * Writes line protocol data to InfluxDB. + * + * @param {string|Array} lines - Line protocol string(s) to write + * @param {string} database - The database to write to + */ +export async function write(lines, database) { + const client = createClient() + try { + const data = Array.isArray(lines) ? lines.join('\n') : lines + await client.write(data, database) + } finally { + await client.close() + } +} + +/** + * Generates a secure application-level token for device authentication. + * In InfluxDB 3 Core, we use application-level tokens stored in the database + * rather than InfluxDB-native authorization tokens. + * + * @returns {string} A secure random token string + */ +export function generateDeviceToken() { + // Generate a secure random token using Node.js crypto + return 'iot_' + randomBytes(32).toString('hex') +} diff --git a/package.json b/package.json index 324a098..427e361 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,20 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start" + "start": "next start", + "test": "jest" }, "dependencies": { - "@influxdata/influxdb-client": "^1.24.0", - "@influxdata/influxdb-client-apis": "^1.24.0", + "@influxdata/influxdb3-client": "^2.0.0", "next": "^16.1.5", "postcss": "^8.4.31", "react": "^18.2.0", "react-dom": "^18.2.0" }, + "devDependencies": { + "jest": "^29.7.0", + "node-mocks-http": "^1.14.1" + }, "resolutions": { "postcss": "^8.4.31" } diff --git a/pages/api/devices/[[...deviceParams]].js b/pages/api/devices/[[...deviceParams]].js index 2474d52..5f34231 100644 --- a/pages/api/devices/[[...deviceParams]].js +++ b/pages/api/devices/[[...deviceParams]].js @@ -1,30 +1,107 @@ -import { getMeasurements } from '../measurements' -import { getDevices } from './_devices' +import { getMeasurements } from '../measurements'; +import { getDevices } from './_devices'; -export default async function handler(req, res) { - try { - const {deviceParams} = req.query - let deviceId = undefined - let path = [] - if(Array.isArray(deviceParams)) { - [deviceId, ...path] = deviceParams +// Maximum query length to prevent abuse +const MAX_QUERY_LENGTH = 2000 + +// Blocked SQL keywords (case-insensitive) +const BLOCKED_PATTERNS = [ + /\b(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|GRANT|REVOKE)\b/i, + /\b(EXEC|EXECUTE|CALL)\b/i, + /;\s*\w/i, // Multiple statements +] + +/** + * Validates a SQL query for safety. + * Only allows SELECT queries without dangerous operations. + * Note: This is basic validation for a sample app - production apps + * should use parameterized queries and proper authorization. + */ +function validateQuery(query) { + if (typeof query !== 'string') { + return { valid: false, error: 'Query must be a string' } + } + + if (query.length > MAX_QUERY_LENGTH) { + return { valid: false, error: `Query exceeds maximum length of ${MAX_QUERY_LENGTH} characters` } + } + + const trimmed = query.trim() + if (!trimmed.toUpperCase().startsWith('SELECT')) { + return { valid: false, error: 'Only SELECT queries are allowed' } + } + + for (const pattern of BLOCKED_PATTERNS) { + if (pattern.test(query)) { + return { valid: false, error: 'Query contains blocked operations' } } - if(Array.isArray(path) && path[0] === 'measurements') { - const {query} = req.body - if(query) { - const data = await getMeasurements(query) - res.status(200).send(data) + } + + return { valid: true } +} + +/** + * API handler for device-related endpoints: + * + * GET /api/devices - List all registered devices + * GET /api/devices/:deviceId - Get a specific device + * POST /api/devices/:deviceId/measurements - Query measurements (SELECT only) + * + * Note: For measurement queries, the `query` parameter must be a SELECT SQL query. + * Flux queries are not supported in InfluxDB 3. + * + * Example SQL query for measurements: + * SELECT * FROM home WHERE room = 'Kitchen' ORDER BY time DESC LIMIT 100 + */ +export default async function handler(req, res) { + try { + const { deviceParams } = req.query; + let deviceId = undefined; + let path = []; + + if (Array.isArray(deviceParams)) { + [deviceId, ...path] = deviceParams; + } + + // Handle measurement queries: POST /api/devices/:deviceId/measurements + if (Array.isArray(path) && path[0] === 'measurements') { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed. Use POST for measurement queries.' }); + } + + const { query } = req.body || {} + if (!query) { + return res.status(400).json({ + error: 'Missing query parameter', + hint: 'Provide a SQL SELECT query in the request body. Flux is not supported in InfluxDB 3.', + example: "SELECT * FROM home WHERE time >= now() - INTERVAL '1 hour' ORDER BY time DESC", + }) } + + // Validate query before execution + const validation = validateQuery(query) + if (!validation.valid) { + return res.status(400).json({ error: validation.error }) + } + + const data = await getMeasurements(query) + res.status(200).send(data) return } - - const devices = await getDevices(deviceId) - res.status(200).json( - Object.values(devices) - .filter((x) => x.deviceId && x.key) // ignore deleted or unknown devices - .sort((a, b) => a.deviceId.localeCompare(b.deviceId)) - ) - } catch(err) { - res.status(500).json({ error: `failed to load data: ${err}` }) - } + + // Handle device listing/retrieval: GET /api/devices or GET /api/devices/:deviceId + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const devices = await getDevices(deviceId); + res.status(200).json( + Object.values(devices) + .filter((x) => x.deviceId && x.key) // Ignore deleted or unknown devices + .sort((a, b) => a.deviceId.localeCompare(b.deviceId)), + ); + } catch (err) { + console.error('Device API error:', err); + res.status(500).json({ error: `Failed to load data: ${err.message || err}` }); + } } diff --git a/pages/api/devices/_devices.js b/pages/api/devices/_devices.js index 587dad4..67c82a0 100644 --- a/pages/api/devices/_devices.js +++ b/pages/api/devices/_devices.js @@ -1,48 +1,82 @@ -import { InfluxDB } from '@influxdata/influxdb-client' -import { flux } from '@influxdata/influxdb-client' +import { query, config } from '../../../lib/influxdb' -const INFLUX_ORG = process.env.INFLUX_ORG -const INFLUX_BUCKET_AUTH = process.env.INFLUX_BUCKET_AUTH -const influxdb = new InfluxDB({url: process.env.INFLUX_URL, token: process.env.INFLUX_TOKEN}) +/** + * Gets devices or a particular device when deviceId is specified. + * Tokens are NEVER returned via public API to prevent unauthorized access. + * + * @param {string} [deviceId] - Optional deviceId to filter by + * @param {Object} [options] - Query options + * @param {boolean} [options.includeToken=false] - Include token (internal use only) + * @returns {Promise>} + */ +export async function getDevices(deviceId, options = {}) { + const { includeToken = false } = options + const database = config.databaseAuth + + // Build SQL query - only include token field for internal verification + let sql + if (deviceId !== undefined) { + const tokenField = includeToken ? ', token' : '' + sql = ` + SELECT time, deviceId, key${tokenField} + FROM deviceauth + WHERE deviceId = '${escapeString(deviceId)}' + ORDER BY time DESC + LIMIT 1 + ` + } else { + // Get all devices - never include tokens in list view + sql = ` + SELECT time, deviceId, key + FROM deviceauth + ORDER BY time DESC + ` + } + + const rows = await query(sql, database) + + // Transform rows into devices object keyed by deviceId + const devices = {} + + for (const row of rows) { + const id = row.deviceId + if (!id) { + continue + } + + // If we already have this device, only update if this row is newer + if (devices[id]) { + const existingTime = new Date(devices[id].updatedAt).getTime() + const rowTime = new Date(row.time).getTime() + if (rowTime <= existingTime) { + continue + } + } + + devices[id] = { + deviceId: id, + key: row.key, + updatedAt: row.time, + } + + // Only include token for internal calls that explicitly request it + if (includeToken && row.token) { + devices[id].token = row.token + } + } + + return devices +} /** - * Gets devices or a particular device when deviceId is specified. Tokens - * are not returned unless deviceId is specified. It can also return devices - * with empty/unknown key, such devices can be ignored (InfluxDB authorization is not associated). - * @param deviceId optional deviceId - * @returns promise with an Record. + * Escapes a string for use in SQL queries to prevent SQL injection. + * @param {string} str - The string to escape + * @returns {string} The escaped string */ - export async function getDevices(deviceId) { - const queryApi = influxdb.getQueryApi(INFLUX_ORG) - const deviceFilter = - deviceId !== undefined - ? flux` and r.deviceId == "${deviceId}"` - : flux` and r._field != "token"` - const fluxQuery = flux`from(bucket:${INFLUX_BUCKET_AUTH}) - |> range(start: 0) - |> filter(fn: (r) => r._measurement == "deviceauth"${deviceFilter}) - |> last()` - const devices = {} - - return await new Promise((resolve, reject) => { - queryApi.queryRows(fluxQuery, { - next(row, tableMeta) { - const o = tableMeta.toObject(row) - const deviceId = o.deviceId - if (!deviceId) { - return - } - const device = devices[deviceId] || (devices[deviceId] = {deviceId}) - device[o._field] = o._value - if (!device.updatedAt || device.updatedAt < o._time) { - device.updatedAt = o._time - } - }, - error: reject, - complete() { - resolve(devices) - }, - }) - }) +function escapeString(str) { + if (typeof str !== 'string') { + return str } - \ No newline at end of file + // Escape single quotes by doubling them + return str.replace(/'/g, "''") +} diff --git a/pages/api/devices/create.js b/pages/api/devices/create.js index 711ba59..324aba2 100644 --- a/pages/api/devices/create.js +++ b/pages/api/devices/create.js @@ -1,74 +1,84 @@ -import { InfluxDB } from '@influxdata/influxdb-client' import { getDevices } from './_devices' -import { AuthorizationsAPI, BucketsAPI } from '@influxdata/influxdb-client-apis' -import { Point } from '@influxdata/influxdb-client' +import { write, config, generateDeviceToken, Point } from '../../../lib/influxdb' -const INFLUX_ORG = process.env.INFLUX_ORG -const INFLUX_BUCKET_AUTH = process.env.INFLUX_BUCKET_AUTH -const INFLUX_BUCKET = process.env.INFLUX_BUCKET - -const influxdb = new InfluxDB({url: process.env.INFLUX_URL, token: process.env.INFLUX_TOKEN}) +// Valid deviceId pattern: alphanumeric, hyphens, underscores, 1-64 chars +const DEVICE_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/ export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + try { - const deviceId = JSON.parse(req.body)?.deviceId - const devices = await createDevice(deviceId) - res.send(200) - } catch(err) { - res.status(500).json({ error: `failed to load data: ${err}` }) - } -} + // Handle both pre-parsed objects and JSON strings + const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body + const deviceId = body?.deviceId -/** Creates an authorization for a deviceId and writes it to a bucket */ -async function createDevice(deviceId) { - let device = (await getDevices(deviceId)) || {} - let authorizationValid = !!Object.values(device)[0]?.key - if(authorizationValid) { - console.log(JSON.stringify(device)) - return Promise.reject('This device ID is already registered and has an authorization.') - } else { - console.log(`createDeviceAuthorization: deviceId=${deviceId}`) - const authorization = await createAuthorization(deviceId) - const writeApi = influxdb.getWriteApi(INFLUX_ORG, INFLUX_BUCKET_AUTH, 'ms', { - batchSize: 2, - }) - const point = new Point('deviceauth') - .tag('deviceId', deviceId) - .stringField('key', authorization.id) - .stringField('token', authorization.token) - writeApi.writePoint(point) - await writeApi.close() - return + const deviceId = body?.deviceId; + if (!deviceId) { + return res.status(400).json({ error: 'deviceId is required' }); + } + + // Validate deviceId format to prevent injection attacks + if (!DEVICE_ID_PATTERN.test(deviceId)) { + return res.status(400).json({ + error: 'Invalid deviceId format', + hint: 'deviceId must be 1-64 characters, alphanumeric with hyphens and underscores only', + }) + } + + const result = await createDevice(deviceId) + res.status(200).json(result) + } catch (err) { + console.error('Device creation error:', err) + res.status(500).json({ error: `Failed to create device: ${err.message || err}` }) } } - /** - * Creates an authorization for a supplied deviceId - * @param {string} deviceId client identifier - * @returns {import('@influxdata/influxdb-client-apis').Authorization} promise with authorization or an error +/** + * Creates a new device with an application-level authentication token. + * + * We store device tokens in the database. + * With InfluxDB 3 Enterprise, we can use fine-grained database tokens. + * With InfluxDB 3 Core, you can use application-level admin tokens. Core doesn't provide fine-grained tokens. + * + * @param {string} deviceId - The unique device identifier + * @returns {Promise<{deviceId: string, key: string, token: string, database: string, host: string, message: string}>} */ - async function createAuthorization(deviceId) { - const authorizationsAPI = new AuthorizationsAPI(influxdb) - const bucketsAPI = new BucketsAPI(influxdb) - const DESC_PREFIX = 'IoTCenterDevice: ' +async function createDevice(deviceId) { + // Check if device already exists + const existingDevices = await getDevices(deviceId); + const existingDevice = Object.values(existingDevices)[0]; - const buckets = await bucketsAPI.getBuckets({name: INFLUX_BUCKET, orgID: INFLUX_ORG}) - const bucketId = buckets.buckets[0]?.id - - return await authorizationsAPI.postAuthorizations({ - body: { - orgID: INFLUX_ORG, - description: DESC_PREFIX + deviceId, - permissions: [ - { - action: 'read', - resource: {type: 'buckets', id: bucketId, orgID: INFLUX_ORG}, - }, - { - action: 'write', - resource: {type: 'buckets', id: bucketId, orgID: INFLUX_ORG}, - }, - ], - }, - }) - } + if (existingDevice?.key) { + throw new Error('This device ID is already registered and has an authorization.'); + } + + console.log(`createDevice: deviceId=${deviceId}`); + + // Generate application-level token for the device + const deviceToken = generateDeviceToken(); + const deviceKey = `device_${deviceId}_${Date.now()}`; + + // Write device auth record using Point class + // Table: deviceauth + // Tags: deviceId + // Fields: key, token + const point = Point.measurement('deviceauth') + .setTag('deviceId', deviceId) + .setStringField('key', deviceKey) + .setStringField('token', deviceToken) + + await write(point.toLineProtocol(), config.databaseAuth) + + console.log(`Device created: ${deviceId}`); + + return { + deviceId, + key: deviceKey, + token: deviceToken, + database: config.database, + host: config.host, + message: 'Device registered successfully. Use the provided token for device authentication.', + }; +} diff --git a/pages/api/measurements/index.js b/pages/api/measurements/index.js index e5ad2e9..b2e3a1b 100644 --- a/pages/api/measurements/index.js +++ b/pages/api/measurements/index.js @@ -1,21 +1,49 @@ -import { InfluxDB } from '@influxdata/influxdb-client' +import { query, config } from '../../../lib/influxdb'; -const INFLUX_ORG = process.env.INFLUX_ORG -const influxdb = new InfluxDB({url: process.env.INFLUX_URL, token: process.env.INFLUX_TOKEN}) +/** + * Executes a SQL query against the InfluxDB 3 database and returns results. + * + * Note: In InfluxDB 3, use SQL or InfluxQL to query data. + * This function accepts SQL queries and returns results as CSV-formatted text + * for backward compatibility with the original API response format. + * + * @param {string} sqlQuery - The SQL query to execute + * @returns {Promise} CSV-formatted query results + */ +export async function getMeasurements(sqlQuery) { + const database = config.database; + const rows = await query(sqlQuery, database); -export async function getMeasurements(fluxQuery) { - const queryApi = influxdb.getQueryApi(INFLUX_ORG) + if (rows.length === 0) { + return ''; + } - return await new Promise((resolve, reject) => { - let result = '' - queryApi.queryLines(fluxQuery, { - next(line) { - result = result.concat(`${line}\n`) - }, - error: reject, - complete() { - resolve(result) - }, - }) - }) - } \ No newline at end of file + // Convert results to CSV format for backward compatibility + const columns = Object.keys(rows[0]); + const header = columns.join(','); + const dataRows = rows.map((row) => columns.map((col) => formatCsvValue(row[col])).join(',')); + + return [header, ...dataRows].join('\n') + '\n'; +} + +/** + * Formats a value for CSV output. + * @param {any} value - The value to format + * @returns {string} The formatted CSV value + */ +function formatCsvValue(value) { + if (value === null || value === undefined) { + return ''; + } + if (typeof value === 'string') { + // Escape quotes and wrap in quotes if contains comma, quote, or newline + if (value.includes(',') || value.includes('"') || value.includes('\n')) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; + } + if (value instanceof Date) { + return value.toISOString(); + } + return String(value); +}