Address spec review findings: SQLite concurrency, TOTP flow, InfluxDB bootstrap

- Add WAL mode + busy timeout for SQLite concurrent access
- Define two-step TOTP login protocol with temporary JWT
- Specify JWT claims structure and error response format
- Add InfluxDB bootstrap via Docker init env vars
- Clarify CORS strategy (same-origin in prod, permissive in dev)
- Add daemon-config.toml to deploy directory
- Clarify event routing (single POST / handler)
- Note sensor delete uses SQLite transaction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Mueller 2026-03-19 22:30:46 +01:00
parent e706b22f31
commit e8e2a7dd6d

View File

@ -73,6 +73,7 @@ LORAWEB_NEU/
├── deploy/ ├── deploy/
│ ├── docker-compose.yml # Project name: loraweb │ ├── docker-compose.yml # Project name: loraweb
│ ├── .env.example │ ├── .env.example
│ ├── daemon-config.toml # Daemon config example (not in git at runtime)
│ └── loraweb.service # systemd unit │ └── loraweb.service # systemd unit
└── docs/ └── docs/
``` ```
@ -83,6 +84,13 @@ LORAWEB_NEU/
Three tables, created idempotently (`CREATE TABLE IF NOT EXISTS`) at startup by both daemon and API. 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 ### topics
Represents a physical LoRaWAN device identified by DevEUI. 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` - `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` - Tags: `dev_eui`, `device_name`, `application_name`
- Timestamp from ChirpStack `time` field - Timestamp from ChirpStack `time` field
- Other events (join, ack, txack, log, status): return 200 OK, no processing - 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 and continue, no crash - InfluxDB errors: log at `warn` level and continue, no crash
### 4.2 Alarm Monitor (Tokio interval loop) ### 4.2 Alarm Monitor (Tokio interval loop)
@ -178,13 +186,24 @@ Axum on port 3001, shared SQLite DB with daemon.
### 5.1 Authentication ### 5.1 Authentication
- `POST /api/auth/login`: Verify username/password (Argon2id) - `POST /api/auth/login`: Single endpoint, two-step flow:
- If TOTP enabled: return `{ totp_required: true }` - **Step 1**: `{ username, password }` → verify Argon2id hash
- Second request with TOTP code → issue JWT - If TOTP not enabled: return `{ token: "jwt..." }`
- If no TOTP: issue JWT directly - If TOTP enabled: return `{ totp_required: true, totp_token: "temporary-token" }`
- JWT: HS256, 24h validity, contains user id + role - **Step 2** (only if TOTP): `{ totp_token, totp_code }` → verify TOTP code against stored secret
- Auth middleware extracts Bearer token, injects user info - `totp_token` is a short-lived JWT (60s, contains user_id, purpose="totp")
- Default admin created at first startup (password from env var) - If valid: return `{ token: "jwt..." }`
- JWT claims: `{ sub: user_id, role: "user"|"admin", exp: timestamp }` (HS256, 24h)
- Auth middleware extracts `Authorization: Bearer <token>`, 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 ### 5.2 Endpoints
@ -195,7 +214,7 @@ Axum on port 3001, shared SQLite DB with daemon.
| /api/sensors | POST | Yes | Create sensor | | /api/sensors | POST | Yes | Create sensor |
| /api/sensors/:id | GET | Yes | Sensor detail | | /api/sensors/:id | GET | Yes | Sensor detail |
| /api/sensors/:id | PUT | Yes | Update sensor | | /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/data | GET | Yes | InfluxDB proxy, ?hours=N |
| /api/sensors/:id/display-config | GET | Yes | Get field config | | /api/sensors/:id/display-config | GET | Yes | Get field config |
| /api/sensors/:id/display-config | PUT | Yes | Save 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 - "Diagramme": Charts per configured panels (D1-D8), time selector, RF metadata toggle
- "Einrichten": Per-field config (label, unit, chart type, panel assignment) - "Einrichten": Per-field config (label, unit, chart type, panel assignment)
4. **UnknownTopicsPage**: List of unapproved topics, dropdown to assign to free sensors 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 5. **AlarmsPage**: Active alarms and warnings list
6. **UsersPage** (admin only): User CRUD, 2FA enable/disable 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: All via `.env` file (not in git). `.env.example` as template with:
- `JWT_SECRET`, `ADMIN_PASSWORD` - `JWT_SECRET`, `ADMIN_PASSWORD`
- `INFLUX_TOKEN`, `INFLUX_ORG`, `INFLUX_BUCKET` - `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.
--- ---