Compare commits

...

2 Commits

Author SHA1 Message Date
bd44740649 API-Auth dazu
automatisch auf INFLUX schalten, wenn es Chi-ID gibt
zuzsätzlich option db=m zum erzwingen von Moing
Anzeige Mongo/Influx im Datenstrom
2025-11-05 09:47:25 +00:00
6d9d94f2fa Anpassungen, damit INFLUX mindestens aml ausgelesen werden kann 2025-11-03 18:45:07 +00:00
13 changed files with 1027 additions and 37 deletions

629
API_DOCUMENTATION.md Normal file
View File

@@ -0,0 +1,629 @@
# SensorAPI - API Dokumentation
## Übersicht
Die SensorAPI ist eine REST-API zur Abfrage von Umweltsensordaten (Lärm, Feinstaub, Temperatur, Radioaktivität). Sie unterstützt sowohl InfluxDB 1.8 (InfluxQL) als auch InfluxDB 2.0 (Flux) sowie MongoDB für Metadaten.
**Version:** 1.4.1
**Basis-URL:** `http://<host>:<port>/api/`
---
## Technologie-Stack
- **Backend:** Node.js mit Express.js
- **Datenbanken:**
- InfluxDB 1.8/2.0 (Zeitreihendaten)
- MongoDB (Sensor-Metadaten und -Eigenschaften)
- **Sprachen:** JavaScript (ES6 Module)
- **Key Dependencies:** axios, luxon, i18next, dotenv
---
## Unterstützte Sensortypen
| Typ | Beschreibung | Messgrößen |
|-----|--------------|------------|
| `noise` | Lärmsensoren | LA_max, LA_min, LA_eq, E10tel_eq |
| `pm` | Feinstaubsensoren | P1, P2, P0 |
| `thp` | Temperatur/Luftfeuchte/Druck | temperature, humidity, pressure |
| `radioactivity` | Radioaktivitätssensoren | counts_per_minute |
---
## API Endpunkte
Alle Endpunkte folgen dem Schema: `GET /api/<command>?<parameter>`
### 1. **getsensordata**
Hauptendpunkt zum Abrufen von Sensordaten. Leitet je nach Sensortyp an spezialisierte Handler weiter.
**Endpunkt:** `/api/getsensordata`
**Pflichtparameter:**
- `sensorid` (int) - Eindeutige Sensor-ID
**Optionale Parameter (abhängig vom Sensortyp):**
#### Für Lärmsensoren (`noise`):
- `data` (string, default: 'live') - Art der Daten
- `span` (int, default: 1) - Zeitspanne in Tagen
- `daystart` (bool, default: null) - Start um 00:00:00 Uhr
- `peak` (int, default: 70) - Schwellenwert für Lärmspitzen in dB
- `since` (date, default: '1900-01-01T00:00:00Z') - Startdatum
- `datetime` (date, default: null) - Spezifisches Datum/Zeit
- `long` (bool, default: false) - Erweiterte Ausgabe
- `sort` (int, default: 1) - Sortierung (1=aufsteigend, -1=absteigend)
- `csv` (bool, default: false) - CSV-Ausgabe
- `out` (string, default: '') - Ausgabeformat
#### Für Radioaktivitätssensoren (`radioactivity`):
- `what` (string, default: 'day') - Art der Aggregation
- `span` (int, default: 1) - Zeitspanne in Tagen
- `avg` (int, default: 1) - Mittelwert-Intervall
- `moving` (bool, default: false) - Gleitender Mittelwert
**Beispiel:**
```
GET /api/getsensordata?sensorid=12345&span=7&peak=75
```
**Antwort:**
```json
{
"err": null,
"values": [
{
"_time": "2025-11-04T10:00:00.000Z",
"DNMS_noise_LA_max": 78.5,
"DNMS_noise_LA_min": 45.2,
"DNMS_noise_LA_eq": 65.3,
"E10tel_eq": 72777980.45
}
]
}
```
---
### 2. **getactdata**
Abrufen aktueller/historischer Rohdaten eines Sensors.
**Endpunkt:** `/api/getactdata`
**Parameter:**
- `sensorid` (int) - Sensor-ID
- `span` (int, optional) - Zeitspanne in Tagen
- `datetime` (date, optional) - Startdatum
- `sort` (int, optional) - Sortierung
**Beispiel:**
```
GET /api/getactdata?sensorid=12345&span=1
```
**Antwort:**
```json
{
"err": null,
"values": [
{
"_time": "2025-11-04T10:00:00.000Z",
"DNMS_noise_LA_max": 78.62,
"DNMS_noise_LA_min": 47.36,
"DNMS_noise_LA_eq": null,
"E10tel_eq": 72777980.45
}
]
}
```
---
### 3. **getavgdata**
Abrufen von Durchschnittswerten mit konfigurierbarem Zeitfenster.
**Endpunkt:** `/api/getavgdata`
**Pflichtparameter:**
- `sensorid` (int) - Sensor-ID
**Optionale Parameter:**
- `span` (int, default: 1) - Zeitspanne in Tagen
- `datetime` (date, default: null) - Startdatum
- `avg` (int, default: 10) - Mittelwert-Intervall in Minuten
- `moving` (bool, default: true) - Gleitender Mittelwert
**Beispiel:**
```
GET /api/getavgdata?sensorid=12345&span=7&avg=60&moving=true
```
**Antwort:**
```json
{
"data": {
"start": "2025-10-28T10:00:00Z",
"span": 7,
"avg": 60,
"moving": true,
"count": 168,
"values": [...]
},
"err": null
}
```
---
### 4. **getlongavg**
Langzeit-Durchschnittswerte über einen längeren Zeitraum.
**Endpunkt:** `/api/getlongavg`
**Pflichtparameter:**
- `sensorid` (int) - Sensor-ID
**Optionale Parameter:**
- `span` (int, default: 2) - Zeitspanne in Tagen
**Beispiel:**
```
GET /api/getlongavg?sensorid=12345&span=30
```
**Antwort:**
```json
{
"data": {
"span": 30,
"values": [
{
"_stop": "2025-11-04T00:00:00.000Z",
"LA_max": 75.3,
"LA_min": 42.1,
"LA_eq": 63.8
}
]
},
"err": null
}
```
---
### 5. **getoneproperty**
Abrufen der Eigenschaften eines Sensors (Metadaten, Standort, etc.).
**Endpunkt:** `/api/getoneproperty`
**Pflichtparameter:**
- `sensorid` (int) - Sensor-ID
**Beispiel:**
```
GET /api/getoneproperty?sensorid=12345
```
**Antwort:**
```json
{
"err": null,
"props": {
"_id": 12345,
"name": [{"name": "Hauptstraße"}],
"type": "noise",
"location": [{
"id": "loc_123",
"loc": {
"type": "Point",
"coordinates": [9.123, 48.456]
},
"indoor": false
}],
"othersensors": [
{"name": "PM Sensor", "sid": 12346}
]
}
}
```
---
### 6. **getmapdata**
Abrufen von Sensordaten für Kartendarstellung mit geografischer Filterung.
**Endpunkt:** `/api/getmapdata`
**Pflichtparameter:**
- `type` (string) - Sensortyp (noise, pm, thp, radioactivity)
**Optionale Parameter (einer muss angegeben werden):**
- `box` (string) - Begrenzungsbox: "west,south,east,north"
- `poly` (JSON-Array) - Polygon-Koordinaten
- `center` (Array) - Mittelpunkt [lng, lat]
- `distance` (int, default: 10) - Radius in km (nur mit `center`)
**Beispiel:**
```
GET /api/getmapdata?type=noise&box=9.0,48.0,9.5,48.5
```
**Antwort:**
```json
{
"err": null,
"options": {
"lastdate": "2025-11-04T10:30:00.000Z",
"count": 25,
"data": "map"
},
"values": [
{
"location": [9.123, 48.456],
"id": 12345,
"name": "Hauptstraße",
"indoor": false,
"lastseen": "2025-11-04T10:25:00.000Z",
"value": 75.3,
"weeks": 0
}
]
}
```
**Wert-Kodierung:**
- `value >= 0`: Aktueller Messwert
- `value = -1`: Daten älter als 2 Stunden
- `value = -2`: Daten älter als 7 Tage
- `value = -3`: Daten älter als 30 Tage
- `value = -4`: Daten älter als 365 Tage
- `value = -5`: Keine Daten vorhanden
---
### 7. **getaddress**
Geocoding: Koordinaten aus Adresse ermitteln.
**Endpunkt:** `/api/getaddress`
**Parameter:**
- `address` (string) - Adresse
**Beispiel:**
```
GET /api/getaddress?address=Hauptstraße+10,+Stuttgart
```
---
### 8. **getcitycoords**
Koordinaten einer Stadt abrufen.
**Endpunkt:** `/api/getcitycoords`
**Parameter:**
- `city` (string) - Stadtname
**Beispiel:**
```
GET /api/getcitycoords?city=Stuttgart
```
---
### 9. **getakwdata**
Abrufen von Atomkraftwerks-Daten (spezialisiert).
**Endpunkt:** `/api/getakwdata`
---
## Zeitformat-Konvertierung
Die API akzeptiert verschiedene Zeitformate:
### InfluxDB 2.0 (Flux) Format:
```
start: -7d
stop: now()
```
### InfluxDB 1.8 (InfluxQL) Format:
```
now() - 7d
now()
```
### Absolute Zeitangaben (ISO 8601):
```
2025-11-04T10:00:00.000Z
```
**Wichtig:** Bei der Verwendung von ISO-Zeitstempeln mit InfluxDB 1.8 werden diese automatisch in Anführungszeichen gesetzt.
---
## Fehlerbehandlung
Alle Endpunkte geben Fehler im folgenden Format zurück:
```json
{
"err": "ERROR_CODE",
"values": []
}
```
**Häufige Fehlercodes:**
- `NODATA` - Keine Daten gefunden
- `SYNTAXURL` - Ungültige Query-Syntax
- `NOPROPSREAD` - Eigenschaften konnten nicht gelesen werden
- `NOPROPSFOUND` - Keine Eigenschaften gefunden
- `CMNDUNKNOWN` - Unbekannter Befehl
---
## Spezielle Berechnungen für Lärmdaten
### Logarithmische Mittelwertbildung
Für Dezibel-Werte (LA_max) wird eine korrekte logarithmische Mittelwertbildung durchgeführt:
1. **Konvertierung zu linear:** `E10tel = 10^(LA_max/10)`
2. **Arithmetischer Mittelwert:** `mean_E10tel = sum(E10tel) / count`
3. **Rückkonvertierung zu dB:** `n_AVG = 10 * log10(mean_E10tel)`
### Peak-Zählung
Bei `getlongavg` mit `long=true` werden Lärmspitzen über einem Schwellenwert gezählt:
```json
{
"_time": "2025-11-04T12:00:00.000Z",
"peakcount": 13,
"n_AVG": 78.51,
"count": 18
}
```
---
## Datenbank-Konfiguration
### Umgebungsvariablen (.env)
```bash
# InfluxDB 1.8
INFLUXHOST=localhost
INFLUXPORT=8086
INFLUXUSER=username
INFLUXPASS=password
INFLUXDB=sensor_data
# InfluxDB 2.0
INFLUXTOKEN=your_token_here
INFLUXDATABUCKET=sensor_data
INFLUXORG=citysensor
# MongoDB
MONGOHOST=localhost
MONGOPORT=27017
MONGODB=sensordb
# Datenbank-Auswahl
DBASE=mongo # oder 'influx'
```
---
## Ausgabeformate
### JSON (Standard)
Alle Endpunkte geben standardmäßig JSON zurück.
### CSV (Optional)
Bei einigen Endpunkten kann `csv=true` übergeben werden:
```
GET /api/getsensordata?sensorid=12345&csv=true
```
---
## Paginierung
Die API verwendet keine explizite Paginierung. Große Datenmengen sollten über `span` und `datetime` Parameter zeitlich eingeschränkt werden.
---
## Rate Limiting
Aktuell ist kein Rate Limiting implementiert. Dies sollte auf Proxy/Load-Balancer-Ebene erfolgen.
---
## Beispiel-Workflows
### Workflow 1: Karte mit Lärmsensoren anzeigen
1. Sensoren in Gebiet abrufen:
```
GET /api/getmapdata?type=noise&box=9.0,48.0,9.5,48.5
```
2. Details für einen Sensor abrufen:
```
GET /api/getoneproperty?sensorid=12345
```
3. Historische Daten abrufen:
```
GET /api/getactdata?sensorid=12345&span=7
```
### Workflow 2: Lärmstatistik über eine Woche
1. Durchschnittswerte mit Lärmspitzen:
```
GET /api/getsensordata?sensorid=12345&span=7&peak=70&long=true
```
2. Ergebnis analysieren:
```json
{
"values": [
{
"_time": "2025-11-04T00:00:00.000Z",
"peakcount": 13,
"n_AVG": 78.51,
"count": 1440,
"LA_max_log_avg": 78.51
}
]
}
```
---
## Internationalisierung
Die API unterstützt mehrere Sprachen über i18next:
- Deutsch (de)
- Englisch (en)
Fehlermeldungen werden automatisch in der gewünschten Sprache zurückgegeben.
---
## Sicherheit
### API-Key-Authentifizierung
Die API unterstützt API-Key-Authentifizierung für programmatischen Zugriff.
#### Aktivierung
1. **API-Keys generieren:**
```bash
node generate-apikey.js 3
```
2. **In .env konfigurieren:**
```bash
API_AUTH_REQUIRED=true
API_KEYS=key1,key2,key3
```
3. **Server neu starten**
#### Verwendung
API-Keys können auf zwei Arten übergeben werden:
**Option 1: HTTP-Header (empfohlen)**
```bash
curl -H "X-API-Key: your-api-key-here" \
"http://localhost:3000/api/getactdata?sensorid=12345"
```
**Option 2: Query-Parameter**
```bash
curl "http://localhost:3000/api/getactdata?sensorid=12345&apikey=your-api-key-here"
```
#### Beispiel mit JavaScript/Node.js
```javascript
import axios from 'axios'
const API_KEY = 'your-api-key-here'
const BASE_URL = 'http://localhost:3000/api'
// Mit Header
const response = await axios.get(`${BASE_URL}/getactdata`, {
params: { sensorid: 12345 },
headers: { 'X-API-Key': API_KEY }
})
// Mit Query-Parameter
const response2 = await axios.get(`${BASE_URL}/getactdata`, {
params: {
sensorid: 12345,
apikey: API_KEY
}
})
```
#### Beispiel mit Python
```python
import requests
API_KEY = 'your-api-key-here'
BASE_URL = 'http://localhost:3000/api'
# Mit Header
headers = {'X-API-Key': API_KEY}
response = requests.get(f'{BASE_URL}/getactdata',
params={'sensorid': 12345},
headers=headers)
# Mit Query-Parameter
response2 = requests.get(f'{BASE_URL}/getactdata',
params={'sensorid': 12345, 'apikey': API_KEY})
```
#### Fehlermeldungen
**401 Unauthorized - Kein API-Key angegeben:**
```json
{
"err": "UNAUTHORIZED",
"message": "API key required. Provide X-API-Key header or apikey query parameter."
}
```
**403 Forbidden - Ungültiger API-Key:**
```json
{
"err": "FORBIDDEN",
"message": "Invalid API key"
}
```
#### Best Practices
- **Niemals API-Keys im Code committen** - verwenden Sie Umgebungsvariablen
- **Verschiedene Keys für verschiedene Clients** - ermöglicht granulare Kontrolle
- **Keys regelmäßig rotieren** - besonders nach Mitarbeiterabgängen
- **HTTPS verwenden** - schützt Keys bei Übertragung
- **Keys sicher speichern** - z.B. in Secret Management Systemen
- **Logging aktivieren** - überwachen Sie API-Zugriffe
### Weitere Sicherheitsmaßnahmen
Für Produktionsumgebungen sollten zusätzlich implementiert werden:
- **HTTPS erzwungen** - verhindert Man-in-the-Middle-Angriffe
- **CORS-Regeln konfiguriert** - beschränkt Browser-Zugriffe
- **Rate Limiting aktiviert** - schützt vor Missbrauch
- **IP-Whitelisting** - zusätzliche Zugriffskontrolle
- **Request-Logging** - Audit-Trail für Compliance
---
## Support und Weiterentwicklung
Für Fragen, Bugs oder Feature-Requests siehe die Projekt-Repository-Dokumentation.
**Version History:**
- 1.4.1: Unterstützung für InfluxDB 1.8 mit InfluxQL, logarithmische Mittelwertbildung für Lärmdaten

186
AUTH_SETUP.md Normal file
View File

@@ -0,0 +1,186 @@
# API-Key-Authentifizierung Setup
## Schnellstart
### 1. API-Keys generieren
```bash
node generate-apikey.js 3
```
Dies generiert 3 zufällige API-Keys wie:
```
1. a1b2c3d4e5f6...
2. f6e5d4c3b2a1...
3. 1a2b3c4d5e6f...
```
### 2. Konfiguration in .env
Kopiere `.env.example` nach `.env` und füge deine Keys hinzu:
```bash
cp .env.example .env
```
Bearbeite `.env`:
```bash
# Aktiviere Authentifizierung
API_AUTH_REQUIRED=true
# Füge generierte Keys hinzu (komma-separiert)
API_KEYS=a1b2c3d4e5f6...,f6e5d4c3b2a1...,1a2b3c4d5e6f...
```
### 3. Server starten/neu starten
```bash
npm start
```
## Verwendung
### Mit curl
```bash
# Header-Methode (empfohlen)
curl -H "X-API-Key: your-key-here" \
"http://localhost:3000/api/getactdata?sensorid=12345"
# Query-Parameter-Methode
curl "http://localhost:3000/api/getactdata?sensorid=12345&apikey=your-key-here"
```
### Mit JavaScript/Node.js
```javascript
import axios from 'axios'
const API_KEY = process.env.SENSOR_API_KEY
const client = axios.create({
baseURL: 'http://localhost:3000/api',
headers: { 'X-API-Key': API_KEY }
})
const data = await client.get('/getactdata', {
params: { sensorid: 12345 }
})
```
### Mit Python
```python
import os
import requests
API_KEY = os.getenv('SENSOR_API_KEY')
headers = {'X-API-Key': API_KEY}
response = requests.get(
'http://localhost:3000/api/getactdata',
params={'sensorid': 12345},
headers=headers
)
```
## Verwaltung
### Neuen Key hinzufügen
1. Generiere neuen Key: `node generate-apikey.js 1`
2. Füge zur `API_KEYS` Liste in `.env` hinzu
3. Restart Server
### Key entfernen
1. Entferne Key aus `API_KEYS` Liste in `.env`
2. Restart Server
### Authentifizierung deaktivieren
```bash
# In .env
API_AUTH_REQUIRED=false
# oder
API_KEYS=
```
## Sicherheit
### ✅ Do's
- ✅ Keys in Umgebungsvariablen speichern
- ✅ Unterschiedliche Keys für verschiedene Clients
- ✅ HTTPS in Produktion verwenden
- ✅ Keys regelmäßig rotieren
- ✅ Zugriffe loggen und überwachen
### ❌ Don'ts
- ❌ Keys in Git committen
- ❌ Keys im Frontend-Code verwenden
- ❌ Denselben Key für alle Clients
- ❌ Keys über unverschlüsselte Verbindungen senden
- ❌ Keys in URLs verwenden (bevorzuge Header)
## Troubleshooting
### "API key required" Fehler
- Stelle sicher, dass der Key im Header oder Query-Parameter übergeben wird
- Prüfe Schreibweise: `X-API-Key` (Header) oder `apikey` (Query)
### "Invalid API key" Fehler
- Prüfe, ob der Key in der `API_KEYS` Liste vorhanden ist
- Stelle sicher, dass keine Leerzeichen oder Zeilenumbrüche im Key sind
- Prüfe, ob `.env` korrekt geladen wird
### Keys werden nicht erkannt
- Server nach `.env` Änderungen neu starten
- Prüfe, ob `dotenv` korrekt konfiguriert ist
- Teste mit: `node -e "console.log(process.env.API_KEYS)"`
## Integration in bestehende Systeme
### Nginx Proxy
```nginx
location /api/ {
proxy_pass http://localhost:3000/api/;
proxy_set_header X-API-Key $http_x_api_key;
}
```
### Apache Proxy
```apache
<Location /api/>
ProxyPass http://localhost:3000/api/
ProxyPassReverse http://localhost:3000/api/
RequestHeader set X-API-Key %{HTTP:X-API-Key}e
</Location>
```
## Erweiterte Konfiguration
### Programmatische Key-Verwaltung
```javascript
import { addApiKey, removeApiKey, generateApiKey } from './utilities/apiauth.js'
// Neuen Key zur Laufzeit hinzufügen
const newKey = generateApiKey()
addApiKey(newKey)
// Key zur Laufzeit entfernen
removeApiKey('old-key-here')
```
### Logging
Authentifizierungs-Ereignisse werden automatisch geloggt:
- Erfolgreiche Authentifizierung
- Fehlgeschlagene Versuche mit IP-Adresse
- Key-Verwaltungsoperationen

View File

@@ -5,7 +5,6 @@ import {returnOnError} from "../utilities/reporterror.js"
import checkParams from "../utilities/checkparams.js"
let readProperties = mongo.readProperties
let readChipData = mongo.readChipData
// Read properties for sensorid and properties for all other sensors on same location
export const getOneProperty = async (params) => {
@@ -14,11 +13,6 @@ export const getOneProperty = async (params) => {
if (err) {
return returnOnError(properties, err, getOneProperty.name)
}
// read 'chip'-data (special for noise sensors)
const chipdata = await readChipData(opts.sensorid)
if (chipdata.err == undefined) {
properties.chip = chipdata
}
let sensorEntries = [];
try {
let pp = await readProperties({sid: opts.sensorid}); // read for given sensorID

View File

@@ -2,7 +2,8 @@
const DBASE = process.env.DBASE || 'mongo'
import {DateTime} from "luxon"
import * as influx from "../databases/influx.js"
import {logit} from "../utilities/logit.js"
import * as influx from "../databases/influx_sql.js"
import * as mongo from "../databases/mongo.js"
import {returnOnError} from "../utilities/reporterror.js"
import {csv2Json} from "../utilities/csv2json.js"
@@ -23,6 +24,7 @@ const noiseParams = [
{name: 'csv', type: 'bool', default: false},
{name: 'long', type: 'bool', default: false},
{name: 'sort', type: 'int', default: 1},
{name: 'db', type: 'string', default: ''},
{name: 'last_seen', type: 'date', default: '1900-01-01T00:00:00Z'},
{name: 'datetime', type: 'date', default: null}
]
@@ -146,17 +148,30 @@ export async function getSensorData(params) {
return returnOnError(ret, 'CMNDUNKNOWN', getActData.name)
}
const getChipPropety = async (opts) => {
// read 'chip'-data (special for noise sensors)
opts.chipid = null
const chipdata = await mongo.readChipData(opts.sensorid)
logit('Ergebnis von readChipData:', chipdata)
if ((chipdata.err === null) && (chipdata.chipdata !== null)) {
opts.chipid = chipdata.chipdata.chip.id
}
if (opts.db === 'm') {
opts.chipid = null
}
return chipdata.err
}
// export const getActData = async (opts) => {
export async function getActData(opts) {
if (DBASE === 'mongo') {
return await mongo.fetchActData(opts)
} else if (DBASE === 'influx') {
const chiperr = await getChipPropety(opts)
if (opts.chipid) {
return await influx.fetchActData(opts)
} else {
return await mongo.fetchActData(opts)
}
return {err: 'DBASEUNKNOWN', values: []}
}

5
app.js
View File

@@ -1,3 +1,4 @@
import 'dotenv/config'
import createError from 'http-errors'
import logger from 'morgan'
import express from 'express'
@@ -19,6 +20,7 @@ import indexRouter from './routes/index.js'
import { apiRouter } from './routes/api.js'
import {fileURLToPath} from "url";
import path from "path";
import { validateApiKey } from './utilities/apiauth.js'
i18next
@@ -42,7 +44,8 @@ app.use(express.urlencoded({ extended: true }))
app.use(cookieParser())
app.use('/', indexRouter)
app.use('/api', apiRouter)
// API-Key-Authentifizierung für alle /api/* Routen
app.use('/api', validateApiKey, apiRouter)
// catch 404 and forward to error handler

View File

@@ -7,6 +7,7 @@
// This implementation converts LA_max to E10tel_eq at runtime to maintain
// compatibility with the Flux version while ensuring correct logarithmic averaging.
import 'dotenv/config'
import axios from 'axios'
import { DateTime } from 'luxon'
import { logit, logerror } from '../utilities/logit.js'
@@ -31,6 +32,7 @@ const INFLUXURL_WRITE = `http://${INFLUXHOST}:${INFLUXPORT}/write`
const influxRead = async (query) => {
let start = DateTime.now()
logit(`ReadInflux from ${INFLUXURL_READ}`)
logit(`Query: ${query}`)
let erg = { values: [], err: null}
try {
@@ -54,17 +56,23 @@ const influxRead = async (query) => {
})
if (ret.status !== 200) {
return returnOnError(erg, 'RESPSTATUS', influxRead.name, ret.status)
erg.err = `RESPSTATUS: ${ret.status}`
logit(`ERROR ${influxRead.name}: ${erg.err}`)
return erg
}
// InfluxDB 1.8 returns JSON format
if (ret.data.error) {
return returnOnError(erg, ret.data.error, influxRead.name)
erg.err = ret.data.error
logit(`ERROR ${influxRead.name}: ${erg.err}`)
return erg
}
erg.values = ret.data.results
} catch (e) {
return returnOnError(erg, e, influxRead.name)
erg.err = e.toString()
logit(`ERROR ${influxRead.name}: ${erg.err}`)
return erg
}
logit(`Influx read time: ${start.diffNow('seconds').toObject().seconds * -1} sec`)
@@ -135,8 +143,9 @@ const transformInfluxResult = (series) => {
columns.forEach((col, index) => {
if (col === 'time') {
// Convert timestamp to ISO string for compatibility
record._time = new Date(row[index]).toISOString()
record.datetime = new Date(row[index]).toISOString()
} else {
col = col.slice(11)
record[col] = row[index]
}
})
@@ -156,15 +165,15 @@ const transformInfluxResult = (series) => {
const fetchFromInflux = async (ret, query) => {
let { values, err } = await influxRead(query)
if (err) {
if (err.toString().includes('400')) {
return returnOnError(ret, 'SYNTAXURL', fetchFromInflux.name)
} else {
return returnOnError(ret, err, fetchFromInflux.name)
}
ret.err = err.toString().includes('400') ? 'SYNTAXURL' : err.toString()
logit(`ERROR ${fetchFromInflux.name}: ${ret.err}`)
return ret
}
logit(`values.length: ${values.length}`)
if (!values || !values.length || !values[0].series) {
return returnOnError(ret, 'NODATA', fetchFromInflux.name)
ret.err = 'NODATA'
logit(`ERROR ${fetchFromInflux.name}: No data returned from query`)
return ret
}
ret.values = transformInfluxResult(values[0].series)
@@ -187,6 +196,14 @@ export const fetchActData = async (opts) => {
let startTime = opts.start.replace('start: ', '').trim()
let stopTime = opts.stop.replace('stop: ', '').trim()
// If time is ISO string, wrap in quotes; if it's a relative time (like now() - 1h), leave as is
if (startTime.match(/^\d{4}-\d{2}-\d{2}T/)) {
startTime = `'${startTime}'`
}
if (stopTime.match(/^\d{4}-\d{2}-\d{2}T/)) {
stopTime = `'${stopTime}'`
}
// Build sorting clause
let orderClause = ''
if (opts.sort) {
@@ -200,9 +217,9 @@ export const fetchActData = async (opts) => {
// InfluxQL query to get LA_max for a sensor within time range
// Note: In InfluxDB 1.8 we only have LA_max, not E10tel_eq like in 2.0
const query = `
SELECT "LA_max", "LA_min", "LA_eq"
FROM "measurements"
WHERE "sid" = '${opts.sensorid}'
SELECT "DNMS_noise_LA_max", "DNMS_noise_LA_min", "DNMS_noise_LAeq"
FROM "DNMS"
WHERE "node" = '${opts.chipid}'
AND time >= ${startTime}
AND time <= ${stopTime}
${orderClause}
@@ -267,22 +284,30 @@ const calculateLogMean = (values) => {
export const fetchNoiseAVGData = async (opts) => {
let ret = { err: null, values: [] }
// Convert Flux time format to InfluxQL format
let startTime = opts.start.replace('start: ', '').trim()
let stopTime = opts.stop.replace('stop: ', '').trim()
// If time is ISO string, wrap in quotes; if it's a relative time (like now() - 1h), leave as is
if (startTime.match(/^\d{4}-\d{2}-\d{2}T/)) {
startTime = `'${startTime}'`
}
if (stopTime.match(/^\d{4}-\d{2}-\d{2}T/)) {
stopTime = `'${stopTime}'`
}
// Since InfluxQL doesn't support complex joins like Flux, we need to make multiple queries
// and combine the results in JavaScript
// Query 1: Get LA_max data aggregated by hour for E10tel calculation
// In InfluxDB 1.8, we only have LA_max (dB), need to convert to E10tel equivalent
const queryLAmaxForE10 = `
SELECT "LA_max", time
FROM "measurements"
WHERE "sid" = '${opts.sensorid}'
SELECT "DNMS_noise_LA_max"
FROM "DNMS"
WHERE "node" = '${opts.chipid}'
AND time >= ${startTime}
AND time <= ${stopTime}
AND "LA_max" IS NOT NULL
ORDER BY time ASC
`
@@ -293,11 +318,15 @@ export const fetchNoiseAVGData = async (opts) => {
// Execute LA_max query (we use the same data for both E10tel calculation and peak counting)
let { values: lamaxValues, err: lamaxErr } = await influxRead(queryLAmaxForE10)
if (lamaxErr) {
return returnOnError(ret, lamaxErr, fetchNoiseAVGData.name)
ret.err = lamaxErr.toString()
logit(`ERROR ${fetchNoiseAVGData.name}: ${ret.err}`)
return ret
}
if (!lamaxValues || !lamaxValues.length || !lamaxValues[0].series) {
return returnOnError(ret, 'NODATA', fetchNoiseAVGData.name)
ret.err = 'NODATA'
logit(`ERROR ${fetchNoiseAVGData.name}: No data returned from query`)
return ret
}
// Transform LA_max results
@@ -323,7 +352,7 @@ export const fetchNoiseAVGData = async (opts) => {
}
}
const lamax = record.LA_max
const lamax = record.DNMS_noise_LA_max || record.LA_max
if (lamax !== null && lamax !== undefined) {
// Store original LA_max value
hourlyData[hourKey].lamaxValues.push(lamax)
@@ -385,7 +414,9 @@ export const fetchNoiseAVGData = async (opts) => {
}
} catch (e) {
return returnOnError(ret, e, fetchNoiseAVGData.name)
ret.err = e.toString()
logit(`ERROR ${fetchNoiseAVGData.name}: ${ret.err}`)
return ret
}
return ret

View File

@@ -55,7 +55,7 @@ export const readProperties = async (query, limit = 0) => {
let client = await connectMongo()
try {
if ("sid" in query) { // if sid is given, read property for sid
ret.properties = await client.db(MONGOBASE).collection('properties_collection').findOne({_id: query.sid})
ret.properties = await client.db(MONGOBASE).collection(properties_collection).findOne({_id: query.sid})
} else { // otherwise read props corresponding to query
ret.properties = await client.db(MONGOBASE).collection(properties_collection).find(query).limit(limit).toArray()
}

18
generate-apikey.js Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env node
// Script to generate API keys for the SensorAPI
import crypto from 'crypto'
const count = parseInt(process.argv[2]) || 1
console.log(`Generating ${count} API Key(s):\n`)
for (let i = 0; i < count; i++) {
const apiKey = crypto.randomBytes(32).toString('hex')
console.log(`${i + 1}. ${apiKey}`)
}
console.log('\nAdd these keys to your .env file:')
console.log('API_KEYS=key1,key2,key3')
console.log('\nOr set API_AUTH_REQUIRED=true to enable authentication')

18
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"cookie-parser": "~1.4.7",
"cors": "^2.8.5",
"debug": "~4.4.3",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"http-errors": "~2.0.0",
"i18next": "^25.5.2",
@@ -2117,6 +2118,18 @@
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==",
"license": "MIT"
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5980,6 +5993,11 @@
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ=="
},
"dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@@ -18,6 +18,7 @@
"cookie-parser": "~1.4.7",
"cors": "^2.8.5",
"debug": "~4.4.3",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"http-errors": "~2.0.0",
"i18next": "^25.5.2",

View File

@@ -7,7 +7,7 @@ import { getActData, getAvgData, getLongAvg, calcRange} from "../actions/getsens
import checkParams from "../utilities/checkparams.js";
import {DateTime} from 'luxon'
import { translate as trans } from '../routes/api.js'
import * as influx from "../databases/influx.js"
import * as influx from "../databases/influx_sql.js"
import * as mongo from "../databases/mongo.js"
import { setoptionfromtable } from "../utilities/chartoptions.js"
@@ -42,6 +42,7 @@ export const getNoiseData = async (params, possibles, props) => {
ret = {
err: erg.err,
options: {
dbase: opts.db === 'm' ? 'Mongo' : 'Influx',
sid: opts.sensorid,
indoor: props.location[0].indoor,
span: opts.span,

View File

@@ -6,7 +6,7 @@ async function testInfluxSQL() {
// Test options similar to what would be used in the application
const testOpts = {
sensorid: 'test_sensor_001',
sensorid: 'esp8266-5829557',
start: 'now() - 1h',
stop: 'now()',
sort: 1,

94
utilities/apiauth.js Normal file
View File

@@ -0,0 +1,94 @@
// API Key Authentication Middleware
import crypto from 'crypto'
import { logit, logerror } from './logit.js'
// API Keys aus Umgebungsvariablen oder Datenbank laden
const API_KEYS = new Set(
(process.env.API_KEYS || '').split(',')
.map(key => key.trim())
.filter(key => key.length > 0)
)
// Optionaler Modus: wenn keine API_KEYS definiert sind, wird keine Auth durchgeführt
const AUTH_REQUIRED = (process.env.API_AUTH_REQUIRED || '').trim().toLowerCase() === 'true'
// Log authentication status on startup
if (AUTH_REQUIRED || API_KEYS.size > 0) {
logit(`API Authentication: ENABLED (${API_KEYS.size} keys configured)`)
} else {
logit(`API Authentication: DISABLED`)
}
/**
* Generate a new API key (for administrative purposes)
* @returns {string} - New API key
*/
export const generateApiKey = () => {
return crypto.randomBytes(32).toString('hex')
}
/**
* Middleware to validate API key
* Accepts API key in:
* - Header: X-API-Key
* - Query parameter: apikey
*/
export const validateApiKey = (req, res, next) => {
// Skip auth if not required
if (!AUTH_REQUIRED && API_KEYS.size === 0) {
return next()
}
// Extract API key from header or query parameter
const apiKey = req.header('X-API-Key') || req.query.apikey
if (!apiKey) {
logit(`ERROR API Auth: No API key provided - ${req.ip}`)
return res.status(401).json({
err: 'UNAUTHORIZED',
message: 'API key required. Provide X-API-Key header or apikey query parameter.'
})
}
// Validate API key
if (!API_KEYS.has(apiKey)) {
logit(`ERROR API Auth: Invalid API key - ${req.ip}`)
return res.status(403).json({
err: 'FORBIDDEN',
message: 'Invalid API key'
})
}
// Log successful authentication
logit(`API Auth: Valid request from ${req.ip}`)
// API key is valid, proceed
next()
}
/**
* Add API key to the allowed list (for runtime management)
* @param {string} apiKey - API key to add
*/
export const addApiKey = (apiKey) => {
API_KEYS.add(apiKey)
logit(`API Key added: ${apiKey.substring(0, 8)}...`)
}
/**
* Remove API key from the allowed list
* @param {string} apiKey - API key to remove
*/
export const removeApiKey = (apiKey) => {
API_KEYS.delete(apiKey)
logit(`API Key removed: ${apiKey.substring(0, 8)}...`)
}
/**
* Check if API authentication is active
* @returns {boolean}
*/
export const isAuthActive = () => {
return AUTH_REQUIRED || API_KEYS.size > 0
}