diff --git a/LORAWEB_NEU/docs/superpowers/specs/2026-03-19-loraweb-design.md b/LORAWEB_NEU/docs/superpowers/specs/2026-03-19-loraweb-design.md index 89b8442..15d5002 100644 --- a/LORAWEB_NEU/docs/superpowers/specs/2026-03-19-loraweb-design.md +++ b/LORAWEB_NEU/docs/superpowers/specs/2026-03-19-loraweb-design.md @@ -73,6 +73,7 @@ LORAWEB_NEU/ ├── deploy/ │ ├── docker-compose.yml # Project name: loraweb │ ├── .env.example +│ ├── daemon-config.toml # Daemon config example (not in git at runtime) │ └── loraweb.service # systemd unit └── docs/ ``` @@ -83,6 +84,13 @@ LORAWEB_NEU/ Three tables, created idempotently (`CREATE TABLE IF NOT EXISTS`) at startup by both daemon and API. +### SQLite Concurrency + +Both daemon and API access the same SQLite file. To handle concurrent access: +- **WAL mode** (`PRAGMA journal_mode=WAL`) enabled at connection open — allows concurrent readers during writes +- **Busy timeout** (`PRAGMA busy_timeout=5000`) — retries on lock contention instead of failing immediately +- Both services set these pragmas on every connection open + ### topics Represents a physical LoRaWAN device identified by DevEUI. @@ -134,8 +142,8 @@ Single Rust binary running two async tasks via Tokio: - `POST /?event=up`: Parse ChirpStack JSON → register unknown DevEUI in `topics` (approved=0) → write all `object` fields + RF metadata (rssi, snr, dr, f_cnt, f_port) to InfluxDB measurement `sensor_data` - Tags: `dev_eui`, `device_name`, `application_name` - Timestamp from ChirpStack `time` field -- Other events (join, ack, txack, log, status): return 200 OK, no processing -- InfluxDB errors: log and continue, no crash +- Routing: single `POST /` handler checks `?event=` query parameter. Only `event=up` is processed; all other values return 200 OK with no further action +- InfluxDB errors: log at `warn` level and continue, no crash ### 4.2 Alarm Monitor (Tokio interval loop) @@ -178,13 +186,24 @@ Axum on port 3001, shared SQLite DB with daemon. ### 5.1 Authentication -- `POST /api/auth/login`: Verify username/password (Argon2id) - - If TOTP enabled: return `{ totp_required: true }` - - Second request with TOTP code → issue JWT - - If no TOTP: issue JWT directly -- JWT: HS256, 24h validity, contains user id + role -- Auth middleware extracts Bearer token, injects user info -- Default admin created at first startup (password from env var) +- `POST /api/auth/login`: Single endpoint, two-step flow: + - **Step 1**: `{ username, password }` → verify Argon2id hash + - If TOTP not enabled: return `{ token: "jwt..." }` + - If TOTP enabled: return `{ totp_required: true, totp_token: "temporary-token" }` + - **Step 2** (only if TOTP): `{ totp_token, totp_code }` → verify TOTP code against stored secret + - `totp_token` is a short-lived JWT (60s, contains user_id, purpose="totp") + - If valid: return `{ token: "jwt..." }` +- JWT claims: `{ sub: user_id, role: "user"|"admin", exp: timestamp }` (HS256, 24h) +- Auth middleware extracts `Authorization: Bearer `, injects user info into request extensions +- Default admin created at first startup (password from `ADMIN_PASSWORD` env var) + +### Error Response Format + +All API errors use a consistent envelope: +```json +{ "error": "Human-readable error message" } +``` +With appropriate HTTP status codes: 400 (validation), 401 (unauthorized), 403 (forbidden), 404 (not found), 500 (internal). ### 5.2 Endpoints @@ -195,7 +214,7 @@ Axum on port 3001, shared SQLite DB with daemon. | /api/sensors | POST | Yes | Create sensor | | /api/sensors/:id | GET | Yes | Sensor detail | | /api/sensors/:id | PUT | Yes | Update sensor | -| /api/sensors/:id | DELETE | Yes | Delete sensor (resets topic to unapproved) | +| /api/sensors/:id | DELETE | Yes | Delete sensor (resets topic to unapproved, in SQLite transaction) | | /api/sensors/:id/data | GET | Yes | InfluxDB proxy, ?hours=N | | /api/sensors/:id/display-config | GET | Yes | Get field config | | /api/sensors/:id/display-config | PUT | Yes | Save field config | @@ -231,6 +250,7 @@ React 18 + TypeScript, Vite 6, Tailwind 4, Recharts. - "Diagramme": Charts per configured panels (D1-D8), time selector, RF metadata toggle - "Einrichten": Per-field config (label, unit, chart type, panel assignment) 4. **UnknownTopicsPage**: List of unapproved topics, dropdown to assign to free sensors + - Sensor edit form shows alarm threshold in **minutes** (only when topic assigned); conversion to hours happens in the API 5. **AlarmsPage**: Active alarms and warnings list 6. **UsersPage** (admin only): User CRUD, 2FA enable/disable @@ -305,7 +325,21 @@ All containers run as user `loraweb` (UID 1001). All via `.env` file (not in git). `.env.example` as template with: - `JWT_SECRET`, `ADMIN_PASSWORD` - `INFLUX_TOKEN`, `INFLUX_ORG`, `INFLUX_BUCKET` -- `DOCKER_INFLUXDB_INIT_*` for InfluxDB bootstrap + +### 7.6 InfluxDB Bootstrap + +InfluxDB 2.7 supports automatic setup via environment variables in Docker: +- `DOCKER_INFLUXDB_INIT_MODE=setup` +- `DOCKER_INFLUXDB_INIT_USERNAME`, `DOCKER_INFLUXDB_INIT_PASSWORD` +- `DOCKER_INFLUXDB_INIT_ORG=loraweb` +- `DOCKER_INFLUXDB_INIT_BUCKET=sensors` +- `DOCKER_INFLUXDB_INIT_ADMIN_TOKEN` (same value as `INFLUX_TOKEN`) + +This creates org, bucket, and admin token on first start. Subsequent starts skip setup if data directory exists. + +### 7.7 CORS + +In production, nginx proxies all `/api/` requests (same origin) — no CORS needed. For local development (Vite dev server on different port), the API includes permissive CORS headers (`tower-http` CorsLayer with `Any` origin). This matches the Pflichtenheft's known limitation E1. ---