From bd44740649c8ab53f198b985aafe2573a3c77412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Wed, 5 Nov 2025 09:47:25 +0000 Subject: [PATCH] =?UTF-8?q?API-Auth=20dazu=20automatisch=20auf=20INFLUX=20?= =?UTF-8?q?schalten,=20wenn=20es=20Chi-ID=20gibt=20zuzs=C3=A4tzlich=20opti?= =?UTF-8?q?on=20db=3Dm=20zum=20erzwingen=20von=20Moing=20Anzeige=20Mongo/I?= =?UTF-8?q?nflux=20im=20Datenstrom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API_DOCUMENTATION.md | 629 +++++++++++++++++++++++++++++++++++++++ AUTH_SETUP.md | 186 ++++++++++++ actions/getproperties.js | 6 - actions/getsensorData.js | 23 +- app.js | 5 +- databases/influx_sql.js | 33 +- generate-apikey.js | 18 ++ sensorspecials/noise.js | 1 + utilities/apiauth.js | 94 ++++++ 9 files changed, 973 insertions(+), 22 deletions(-) create mode 100644 API_DOCUMENTATION.md create mode 100644 AUTH_SETUP.md create mode 100755 generate-apikey.js create mode 100644 utilities/apiauth.js diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..1fec861 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -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://:/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/?` + +### 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 diff --git a/AUTH_SETUP.md b/AUTH_SETUP.md new file mode 100644 index 0000000..4d4a510 --- /dev/null +++ b/AUTH_SETUP.md @@ -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 + + ProxyPass http://localhost:3000/api/ + ProxyPassReverse http://localhost:3000/api/ + RequestHeader set X-API-Key %{HTTP:X-API-Key}e + +``` + +## 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 diff --git a/actions/getproperties.js b/actions/getproperties.js index cdaa5e1..bb42bdc 100644 --- a/actions/getproperties.js +++ b/actions/getproperties.js @@ -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 diff --git a/actions/getsensorData.js b/actions/getsensorData.js index 6895e48..9018ef6 100644 --- a/actions/getsensorData.js +++ b/actions/getsensorData.js @@ -2,6 +2,7 @@ const DBASE = process.env.DBASE || 'mongo' import {DateTime} from "luxon" +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" @@ -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: []} } diff --git a/app.js b/app.js index 6e9bfa4..3ecdb3e 100644 --- a/app.js +++ b/app.js @@ -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 diff --git a/databases/influx_sql.js b/databases/influx_sql.js index 0fd022f..9692ac9 100644 --- a/databases/influx_sql.js +++ b/databases/influx_sql.js @@ -143,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] } }) @@ -168,7 +169,7 @@ const fetchFromInflux = async (ret, query) => { logit(`ERROR ${fetchFromInflux.name}: ${ret.err}`) return ret } - + logit(`values.length: ${values.length}`) if (!values || !values.length || !values[0].series) { ret.err = 'NODATA' logit(`ERROR ${fetchFromInflux.name}: No data returned from query`) @@ -195,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) { @@ -208,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 "DNMS_noise_LA_max", "DNMS_noise_LA_min", "DNMS_noise_LA_eq" + SELECT "DNMS_noise_LA_max", "DNMS_noise_LA_min", "DNMS_noise_LAeq" FROM "DNMS" - WHERE "node" = '${opts.sensorid}' + WHERE "node" = '${opts.chipid}' AND time >= ${startTime} AND time <= ${stopTime} ${orderClause} @@ -275,17 +284,19 @@ const calculateLogMean = (values) => { export const fetchNoiseAVGData = async (opts) => { let ret = { err: null, values: [] } - // convert sensorID ti esp-chip-is, if possible - const convert2espid = (opts) => { - const sid = opts.sensorid - - - } // 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 @@ -294,7 +305,7 @@ export const fetchNoiseAVGData = async (opts) => { const queryLAmaxForE10 = ` SELECT "DNMS_noise_LA_max" FROM "DNMS" - WHERE "node" = '${opts.sensorid}' + WHERE "node" = '${opts.chipid}' AND time >= ${startTime} AND time <= ${stopTime} ORDER BY time ASC diff --git a/generate-apikey.js b/generate-apikey.js new file mode 100755 index 0000000..82c9151 --- /dev/null +++ b/generate-apikey.js @@ -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') diff --git a/sensorspecials/noise.js b/sensorspecials/noise.js index 70066bc..1ec9220 100644 --- a/sensorspecials/noise.js +++ b/sensorspecials/noise.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, diff --git a/utilities/apiauth.js b/utilities/apiauth.js new file mode 100644 index 0000000..80ed2f7 --- /dev/null +++ b/utilities/apiauth.js @@ -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 +}