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);
+}