Compare commits

...

10 Commits

Author SHA1 Message Date
rxf
633e1ea4f3 package.json updated 2025-09-02 19:06:32 +02:00
rxf
5ccd37b931 Nun mit login und Einrichten zusätzlicher User 2025-09-02 18:49:50 +02:00
rxf
6466bb2d92 V 1.1.0 - Bearbeiten bei Liste geht, auch Löschen. 2025-09-01 18:47:31 +02:00
rxf
90662d8d0b Liste sortieren, bearbeiten 2025-08-24 14:55:42 +02:00
6c0aaf0123 *** WOP ***
Liste bearbeiten geht noch nicht
2025-08-21 15:11:14 +00:00
326b30206d V1.0.0 - zum Testen ausgelifert via esp2sensor.fuerst-stuttgart.de 2025-08-19 16:13:23 +00:00
4a47d3cd86 Beschreibung dazu
Typo in adress.js korrigiert
2025-08-19 16:11:25 +00:00
e7e6cb97ca kann deployed werde (zum ersten Mal) 2025-08-19 13:43:37 +00:00
b3b411db1a Eingabe und Listing funktionieren erst mal 2025-08-19 07:28:29 +00:00
61f8dec32f Mongo-Zugriff passt jetzt
Adresse wird richtig geholt
Daten werden in prop_flux gespeichert
2025-08-18 13:35:59 +00:00
24 changed files with 1840 additions and 857 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules node_modules
.env .env
*.pdf

63
build_and_copy.sh Executable file
View File

@@ -0,0 +1,63 @@
#!/bin/bash
# Build Docker-Container
#
# Call: buildit.sh name [target]
#
# The Dockerfile must be named like Dockerfile_name
#
# 2018-09-20 rxf
# - before sending docker image to remote, tag actual remote image
#
# 2018-09-14 rxf
# - first Version
#
set -x
port=""
orgName=esp2sensor
name=esp2sensor
usage()
{
echo "Usage build_and_copy.sh [-p port] [-n name] target"
echo " Build docker container $name and copy to target"
echo "Params:"
echo " target: Where to copy the container to "
echo " -p port: ssh port (default 22)"
echo " -n name: new name for container (default: $orgName)"
}
while getopts n:p:h? o
do
case "$o" in
n) name="$OPTARG";;
p) port="-p $OPTARG";;
h) usage; exit 0;;
*) usage; exit 1;;
esac
done
shift $((OPTIND-1))
while [ $# -gt 0 ]; do
if [[ -z "$target" ]]; then
target=$1
shift
else
echo "bad option $1"
# exit 1
shift
fi
done
docker build -f Dockerfile_$orgName --no-cache -t $name .
dat=`date +%Y%m%d%H%M`
if [ "$target" == "localhost" ]
then
docker tag $name $name:V_$dat
exit
fi
ssh $port $target "docker tag $name $name:V_$dat"
docker save $name | bzip2 | pv | ssh $port $target 'bunzip2 | docker load'

View File

@@ -1,30 +1,60 @@
import { MongoClient } from 'mongodb'; import { MongoClient } from 'mongodb';
import dotenv from 'dotenv';
const MONGO_ROOT_USER = process.env.MONGO_ROOT_USER; dotenv.config();
const MONGO_ROOT_PASSWORD = process.env.MONGO_ROOT_PASSWORD;
let MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
if (MONGO_ROOT_USER && MONGO_ROOT_PASSWORD) { const MONGOHOST = process.env.MONGOHOST || 'localhost'
const uriParts = MONGO_URI.split('://'); const MONGOPORT = process.env.MONGOPORT || 27017
if (uriParts.length === 2) { const MONGOAUTH = process.env.MONGOAUTH || false
const protocol = uriParts[0]; const MONGOUSRP = process.env.MONGOUSRP || ''
const rest = uriParts[1]; const MONGOBASE = process.env.MONGOBASE || 'sensor_data'
MONGO_URI = `${protocol}://${encodeURIComponent(MONGO_ROOT_USER)}:${encodeURIComponent(MONGO_ROOT_PASSWORD)}@${rest}`; let MONGO_URL = 'mongodb://'+MONGOHOST+':'+MONGOPORT; // URL to mongo database
if (MONGOAUTH === 'true') {
MONGO_URL = 'mongodb://'+MONGOUSRP+'@' + MONGOHOST + ':' + MONGOPORT + '/?authSource=admin'; // URL to mongo database
} }
} const DB_NAME = MONGOBASE
const DB_NAME = process.env.DB_NAME || 'espdb';
let db, entriesCollection, usersCollection; let db, usersCollection, prop_fluxCollection, propertiesCollection;
let client = null;
export async function initMongo() { export async function initMongo() {
const client = new MongoClient(MONGO_URI); const client = new MongoClient(MONGO_URL);
await client.connect(); await client.connect();
db = client.db(DB_NAME); db = client.db(DB_NAME);
entriesCollection = db.collection('entries'); usersCollection = db.collection('user');
usersCollection = db.collection('users'); prop_fluxCollection = db.collection('prop_flux');
return { db, entriesCollection, usersCollection }; propertiesCollection = db.collection('properties')
return { db, usersCollection, prop_fluxCollection, propertiesCollection};
} }
export function getCollections() { export const clientClose = async () => {
return { db, entriesCollection, usersCollection }; if (client) {
client.close()
}
}
export function getCollections() {
return { db, usersCollection, prop_fluxCollection, propertiesCollection};
}
export const update_pflux = async(sn, doc) => {
try {
await prop_fluxCollection.updateOne({_id: sn},{ $set: { 'chip': doc}})
return {"error": null}
} catch (e) {
return { "error": true, "what": e}
}
}
export const get_pflux = async(sn) => {
try {
let r = await prop_fluxCollection.findOne({_id: sn})
if (r == null) {
return { "error": true, "what": "Not found", "erg": r}
}
return {"error": null, "what": null, "erg": r}
} catch (e) {
return { "error": true, "what": e, "erg": null}
}
} }

36
deploy.sh Executable file
View File

@@ -0,0 +1,36 @@
# Deplay ein Sensor-File auf das docker registry (docker.citysensor.de)
#
# v 1.0 2024-09-01 rxf
# erste Version
#set -x
registry=docker.citysensor.de
name=esp2sensor
usage()
{
echo "Usage ./deploy.sh"
echo " Build docker container '$name' and deploy to $registry"
echo "Params:"
echo " -h show this usage"
}
while getopts h? o
do
case "$o" in
h) usage; exit 0;;
*) usage; exit 1;;
esac
done
shift $((OPTIND-1))
./build_and_copy.sh localhost
docker tag $name docker.citysensor.de/$name:latest
dat=`date +%Y%m%d%H%M`
docker tag $name docker.citysensor.de/$name:V_$dat
docker push docker.citysensor.de/$name

View File

@@ -1,18 +1,15 @@
services: services:
app: app:
build: . build: .
container_name: esp-app container_name: esp2sensor
ports: ports:
- "3000:3000" - "3000:3000"
environment:
- PORT=3000
- MONGO_URI=mongodb://mongo:27017
- DB_NAME=espdb
depends_on: depends_on:
- mongo - mongo
volumes: volumes:
- .:/app # bind mount für Live-Reload - .:/app # bind mount für Live-Reload
- /app/node_modules # node_modules vom Host nicht überschreiben - /app/node_modules # node_modules vom Host nicht überschreiben
- ./log:/var/log
restart: unless-stopped restart: unless-stopped
mongo: mongo:

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

37
docs/beschreibung.md Normal file
View File

@@ -0,0 +1,37 @@
# Beschreibung von esp2senor
### Aufruf
Für die Tests:
```
esp2sensor.fuerst-stuttgart.de
```
### Darstellung
Die **Eingabe**-Seite sieht folgendermaßen aus:
![eingabe](ESP2SENSOR_PICS/eingabe.png)
* In die Zeile **Sensornummer** wird die Sensornummer (die von sensor.community) eingetragen. Das Programm sucht daraufhin, ob die Sensornummer in der bisherigen Datenbank bekannt ist. Wenn ja, wird die Adresse aus den Koordinaten gelesen und in der Zeile **Anschrift** angezeigt. Falls nicht, erfolgt eine Fehlermeldung.
* Nun können die rerstlichen Daten, nämlich ESP-ID, Bezeichnung und Beschreibung eigegeben werden. Nur die ESP_ID **muss** eingegeben werden, die anderen beiden sind optional. Die Anschrift kann *nicht* eingegeben und *nicht* verändert werden.
* Durch Klick auf **Speichern** werden die Daten in die Datenbank übernommen. Eventuell schon in der DB vorhandene Daten werden überschrieben. Die Anschrift wird *nicht* gespeichert, da diese sich immer aus den schon vohandenen Koordinaten berechnet.
* Sollte die Sensornummer schon mal eingegeben worden sein, so werden alle eingegebenen Daten aus der Datenbank geholt und angezeigt. Sie können hier nun auch geändert werden (außer Anschrift).
Für die Speicherung dieser neuen Daten wird eine extra 'Collection' namens *prop_flux* verwendet. Die Koordinaten für die Adresse werden aus der Standard-Collection *properties* geholt. Alle in dieser Collection vohandenen Daten werde aus *properties* nach *prop_flux* kopiert und um die Eingaben ergänzt.
Dies ist schon mal eine Vorebreitung für eine zukünftige Sensor-Verwaltung.
Die zweite Seite ist die **Liste**:
![eingabe](ESP2SENSOR_PICS/liste.png)
Je mehr Daten vorliegen, desto länger wird die Liste. Über **Aktualisieren** kann die Liste neu geladen werden. Über **Seite** kann die angezeigte Seite angewählt werde, über **Limit** wird die Anzahl der Einträge einer Seite festgelegt.
Angezeigt werden die eingegebenen Daten aus der Collecction *prop_flux*. Es wird nach dem Eingabe-Datum absteigend sortiert.
#### History
| Version | Datum | Author |Bemerkung|
|---------|-------|-----|----------|
|1.0.0 | 2025-08-19 | rxf | erste Fassung |

3
hashpasswd Normal file
View File

@@ -0,0 +1,3 @@
import bcrypt from 'bcrypt';
const hashedPassword = await bcrypt.hash('Tux4esp', 10);
console.log(hashedPassword)

311
log/esp2sensor.log Normal file
View File

@@ -0,0 +1,311 @@
[dotenv@17.2.1] injecting env (10) from .env -- tip: 🛠️ run anywhere with `dotenvx run -- yourcommand`
[dotenv@17.2.1] injecting env (0) from .env -- tip: 🔐 prevent building .env in docker: https://dotenvx.com/prebuild
/app/node_modules/mongodb/lib/sdam/topology.js:326
const timeoutError = new error_1.MongoServerSelectionError(`Server selection timed out after ${timeout?.duration} ms`, this.description);
^
MongoServerSelectionError: connect ECONNREFUSED 192.168.16.2:2017
at Topology.selectServer (/app/node_modules/mongodb/lib/sdam/topology.js:326:38)
at async Topology._connect (/app/node_modules/mongodb/lib/sdam/topology.js:200:28)
at async Topology.connect (/app/node_modules/mongodb/lib/sdam/topology.js:152:13)
at async topologyConnect (/app/node_modules/mongodb/lib/mongo_client.js:258:17)
at async MongoClient._connect (/app/node_modules/mongodb/lib/mongo_client.js:271:13)
at async MongoClient.connect (/app/node_modules/mongodb/lib/mongo_client.js:196:13)
at async initMongo (file:///app/db/mongo.js:21:3)
at async file:///app/server.js:35:1 {
errorLabelSet: Set(0) {},
reason: TopologyDescription {
type: 'Unknown',
servers: Map(1) {
'mongo:2017' => ServerDescription {
address: 'mongo:2017',
type: 'Unknown',
hosts: [],
passives: [],
arbiters: [],
tags: {},
minWireVersion: 0,
maxWireVersion: 0,
roundTripTime: -1,
minRoundTripTime: 0,
lastUpdateTime: 72112013,
lastWriteDate: 0,
error: MongoNetworkError: connect ECONNREFUSED 192.168.16.2:2017
at Socket.<anonymous> (/app/node_modules/mongodb/lib/cmap/connect.js:286:44)
at Object.onceWrapper (node:events:639:26)
at Socket.emit (node:events:524:28)
at emitErrorNT (node:internal/streams/destroy:169:8)
at emitErrorCloseNT (node:internal/streams/destroy:128:3)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
errorLabelSet: Set(1) { 'ResetPool' },
beforeHandshake: false,
[cause]: Error: connect ECONNREFUSED 192.168.16.2:2017
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1611:16) {
errno: -111,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '192.168.16.2',
port: 2017
}
},
topologyVersion: null,
setName: null,
setVersion: null,
electionId: null,
logicalSessionTimeoutMinutes: null,
maxMessageSizeBytes: null,
maxWriteBatchSize: null,
maxBsonObjectSize: null,
primary: null,
me: null,
'$clusterTime': null,
iscryptd: false
}
},
stale: false,
compatible: true,
heartbeatFrequencyMS: 10000,
localThresholdMS: 15,
setName: null,
maxElectionId: null,
maxSetVersion: null,
commonWireVersion: 0,
logicalSessionTimeoutMinutes: null
},
code: undefined,
[cause]: MongoNetworkError: connect ECONNREFUSED 192.168.16.2:2017
at Socket.<anonymous> (/app/node_modules/mongodb/lib/cmap/connect.js:286:44)
at Object.onceWrapper (node:events:639:26)
at Socket.emit (node:events:524:28)
at emitErrorNT (node:internal/streams/destroy:169:8)
at emitErrorCloseNT (node:internal/streams/destroy:128:3)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
errorLabelSet: Set(1) { 'ResetPool' },
beforeHandshake: false,
[cause]: Error: connect ECONNREFUSED 192.168.16.2:2017
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1611:16) {
errno: -111,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '192.168.16.2',
port: 2017
}
}
}
Node.js v20.19.4
[dotenv@17.2.1] injecting env (10) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ override existing env vars with { override: true }
[dotenv@17.2.1] injecting env (10) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
[dotenv@17.2.1] injecting env (0) from .env -- tip: 📡 version env with Radar: https://dotenvx.com/radar
Server läuft auf http://localhost:3000
Address lookup failed: TypeError: fetch failed
at node:internal/deps/undici/undici:13510:13
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async file:///app/routes/address.js:44:19 {
[cause]: Error: getaddrinfo EAI_AGAIN sensorapi
at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:120:26) {
errno: -3001,
code: 'EAI_AGAIN',
syscall: 'getaddrinfo',
hostname: 'sensorapi'
}
}
file:///app/routes/address.js:59
await client.close();
^
ReferenceError: client is not defined
at file:///app/routes/address.js:59:5
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Node.js v20.19.4
[dotenv@17.2.1] injecting env (10) from .env -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommit
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ load multiple .env files with { path: ['.env.local', '.env'] }
Server läuft auf http://localhost:3000
Address lookup failed: TypeError: fetch failed
at node:internal/deps/undici/undici:13510:13
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async file:///app/routes/address.js:44:19 {
[cause]: Error: getaddrinfo EAI_AGAIN sensorapi
at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:120:26) {
errno: -3001,
code: 'EAI_AGAIN',
syscall: 'getaddrinfo',
hostname: 'sensorapi'
}
}
file:///app/routes/address.js:59
await client.close();
^
ReferenceError: client is not defined
at file:///app/routes/address.js:59:5
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Node.js v20.19.4
[dotenv@17.2.1] injecting env (10) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ load multiple .env files with { path: ['.env.local', '.env'] }
Server läuft auf http://localhost:3000
[dotenv@17.2.1] injecting env (9) from .env -- tip: 🔐 prevent building .env in docker: https://dotenvx.com/prebuild
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ enable debug logging with { debug: true }
Server läuft auf http://localhost:3000
[dotenv@17.2.1] injecting env (9) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
Server läuft auf http://localhost:3000
[dotenv@17.2.1] injecting env (9) from .env -- tip: ⚙️ suppress all logs with { quiet: true }
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ enable debug logging with { debug: true }
Server läuft auf http://localhost:3000
https://noise.fuerst-stuttgart.de/srv/getaddressgetaddress/?sensorid=37833
https://noise.fuerst-stuttgart.de/srv/getaddressgetaddress/?sensorid=37833
[dotenv@17.2.1] injecting env (9) from .env -- tip: ⚙️ write to custom object with { processEnv: myObject }
[dotenv@17.2.1] injecting env (0) from .env -- tip: ⚙️ write to custom object with { processEnv: myObject }
Server läuft auf http://localhost:3000
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=37833
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=79222
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=79222
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=79222
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=79222
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=79222
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=37833
https://noise.fuerst-stuttgart.de/srv/getaddress/?sensorid=37833
node:internal/modules/cjs/loader:1215
throw err;
^
Error: Cannot find module '/app/server.js'
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
at Module._load (node:internal/modules/cjs/loader:1043:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
at node:internal/main/run_main_module:28:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v20.19.4
node:internal/modules/cjs/loader:1215
throw err;
^
Error: Cannot find module '/app/server.js'
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
at Module._load (node:internal/modules/cjs/loader:1043:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
at node:internal/main/run_main_module:28:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v20.19.4
node:internal/modules/cjs/loader:1215
throw err;
^
Error: Cannot find module '/app/server.js'
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
at Module._load (node:internal/modules/cjs/loader:1043:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
at node:internal/main/run_main_module:28:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v20.19.4
node:internal/modules/cjs/loader:1215
throw err;
^
Error: Cannot find module '/app/server.js'
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
at Module._load (node:internal/modules/cjs/loader:1043:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
at node:internal/main/run_main_module:28:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v20.19.4
node:internal/modules/cjs/loader:1215
throw err;
^
Error: Cannot find module '/app/server.js'
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
at Module._load (node:internal/modules/cjs/loader:1043:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
at node:internal/main/run_main_module:28:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v20.19.4
node:internal/modules/cjs/loader:1215
throw err;
^
Error: Cannot find module '/app/server.js'
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
at Module._load (node:internal/modules/cjs/loader:1043:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
at node:internal/main/run_main_module:28:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v20.19.4
node:internal/modules/cjs/loader:1215
throw err;
^
Error: Cannot find module '/app/server.js'
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
at Module._load (node:internal/modules/cjs/loader:1043:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
at node:internal/main/run_main_module:28:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v20.19.4
node:internal/modules/cjs/loader:1215
throw err;
^
Error: Cannot find module '/app/server.js'
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
at Module._load (node:internal/modules/cjs/loader:1043:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
at node:internal/main/run_main_module:28:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v20.19.4
node:internal/modules/cjs/loader:1215
throw err;
^
Error: Cannot find module '/app/server.js'
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
at Module._load (node:internal/modules/cjs/loader:1043:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
at node:internal/main/run_main_module:28:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v20.19.4
node:internal/modules/cjs/loader:1215
throw err;
^
Error: Cannot find module '/app/server.js'
at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
at Module._load (node:internal/modules/cjs/loader:1043:27)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12)
at node:internal/main/run_main_module:28:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v20.19.4

1321
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
{ {
"name": "espid2sensor", "name": "espid2sensor",
"version": "1.0.0", "version": "1.2.0",
"date": "2025-09-02 17:00 UTC",
"type": "module", "type": "module",
"description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB", "description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js >>/var/log/esp2sensor.log 2>&1",
"dev": "nodemon --watch server.js --watch views --watch public server.js", "dev": "nodemon --watch server.js --watch views --watch public server.js",
"test": "jest" "test": "jest"
}, },
@@ -13,14 +14,14 @@
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"express": "^4.18.2", "express": "^5.1.0",
"express-session": "^1.18.2", "express-session": "^1.18.2",
"mongodb": "^6.6.0", "mongodb": "^6.19.0",
"pug": "^3.0.2" "pug": "^3.0.3"
}, },
"devDependencies": { "devDependencies": {
"jest": "^30.0.5", "jest": "^30.1.3",
"nodemon": "^3.0.1", "nodemon": "^3.1.10",
"supertest": "^7.1.4" "supertest": "^7.1.4"
} }
} }

View File

@@ -1,3 +1,65 @@
// Tab-Wechsel Funktion aus index.pug
function showTab(tab) {
document.getElementById('tabInputContent').style.display = tab === 'input' ? '' : 'none';
document.getElementById('tabListContent').style.display = tab === 'list' ? '' : 'none';
const tabUserContent = document.getElementById('tabUserContent');
if (tabUserContent) tabUserContent.style.display = tab === 'user' ? '' : 'none';
document.getElementById('tabInput').classList.toggle('active', tab === 'input');
document.getElementById('tabList').classList.toggle('active', tab === 'list');
const tabUser = document.getElementById('tabUser');
if (tabUser) tabUser.classList.toggle('active', tab === 'user');
}
// User-Tab Handling (nur für Admins)
document.addEventListener('DOMContentLoaded', () => {
const userSaveBtn = document.getElementById('userSaveBtn');
if (userSaveBtn) {
userSaveBtn.addEventListener('click', async () => {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
const role = document.getElementById('role').value;
const userResult = document.getElementById('userResult');
if (!username || !password) {
userResult.textContent = 'Benutzername und Passwort erforderlich.';
return;
}
try {
const res = await fetch('/api/createUser', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, role })
});
const data = await res.json();
if (data.success) {
userResult.textContent = 'User erfolgreich angelegt!';
} else {
userResult.textContent = data.error || 'Fehler beim Anlegen.';
}
} catch (err) {
userResult.textContent = 'Serverfehler.';
}
});
}
});
function updateSortArrows() {
const arrows = {
sensorNr: document.getElementById('sortArrowSensorNr'),
espId: document.getElementById('sortArrowEspId'),
date: document.getElementById('sortArrowDate')
};
Object.entries(arrows).forEach(([key, el]) => {
if (!el) return;
// Aktiver Pfeil fett, andere ausgegraut
el.textContent = currentSort.key === key
? (currentSort.asc ? '↑' : '↓')
: '↑';
el.style.fontWeight = currentSort.key === key ? 'bold' : 'normal';
el.style.opacity = currentSort.key === key ? '1' : '0.3';
});
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const saveBtn = document.getElementById('saveBtn'); const saveBtn = document.getElementById('saveBtn');
const refreshBtn = document.getElementById('refreshBtn'); const refreshBtn = document.getElementById('refreshBtn');
@@ -10,12 +72,13 @@ document.addEventListener('DOMContentLoaded', () => {
const limitInput = document.getElementById('limit'); const limitInput = document.getElementById('limit');
const resultDiv = document.getElementById('result'); const resultDiv = document.getElementById('result');
const tableBody = document.querySelector('#entriesTable tbody'); const tableBody = document.querySelector('#entriesTable tbody');
const tabInput = document.getElementById('tabInput');
const tabList = document.getElementById('tabList');
let editId = null;
// Modal für Fehleranzeige // Modal für Fehleranzeige
function showModal(message, callback) { function showModal(message, showCancelButton, callback) {
// Vorherige Modals entfernen // Remove previous modals
document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove()); document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove());
let modal = document.createElement('div'); let modal = document.createElement('div');
@@ -29,20 +92,51 @@ document.addEventListener('DOMContentLoaded', () => {
msg.textContent = message; msg.textContent = message;
box.appendChild(msg); box.appendChild(msg);
let btn = document.createElement('button'); let btndiv = document.createElement('div')
btn.className = 'custom-modal-btn'; btndiv.className = 'twobuttons'
btn.textContent = 'OK';
btn.onclick = () => { // Cancel Button (only if showCancelButton is true)
if (showCancelButton) {
let btnCancel = document.createElement('button');
btnCancel.className = 'custom-modal-btn';
btnCancel.textContent = 'Abbruch';
btnCancel.onclick = () => {
if (modal.parentNode) { if (modal.parentNode) {
modal.parentNode.removeChild(modal); modal.parentNode.removeChild(modal);
} }
if (callback) callback(); if (callback) callback(false); // Pass false for Cancel
}; };
box.appendChild(btn); btndiv.appendChild(btnCancel);
modal.appendChild(box);
document.body.appendChild(modal);
} }
// OK Button
let btnOk = document.createElement('button');
btnOk.className = 'custom-modal-btn';
btnOk.textContent = 'OK';
btnOk.onclick = () => {
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
if (callback) callback(true); // Pass true for OK
};
btndiv.appendChild(btnOk);
box.appendChild(btndiv)
modal.appendChild(box);
document.body.appendChild(modal);
// Optional: Close modal when clicking outside
modal.onclick = (e) => {
if (e.target === modal) {
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
if (callback) callback(false); // Treat as cancel
}
};
}
// Sensornummer nur Zahlen erlauben // Sensornummer nur Zahlen erlauben
sensorNumberInput.addEventListener('input', () => { sensorNumberInput.addEventListener('input', () => {
sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, ''); sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, '');
@@ -55,12 +149,23 @@ async function fetchAddressIfValid() {
try { try {
const res = await fetch(`/api/address/${value}`); const res = await fetch(`/api/address/${value}`);
const data = await res.json(); const data = await res.json();
console.dir(data)
if (!data.error && data.address) { if (!data.error && data.address) {
addressInput.value = data.address; addressInput.value = data.address;
// Felder automatisch füllen, wenn props vorhanden
if (!data.props.error) {
if (data.props.erg.chip !== undefined) {
let pp = data.props.erg.chip
espIdInput.value = pp.id || ''
nameInput.value = pp.name || ''
descriptionInput.value = pp.description || ''
// Weitere Felder nach Bedarf
}
}
} else { } else {
addressInput.value = ''; addressInput.value = '';
sensorNumberInput.disabled = true; sensorNumberInput.disabled = true;
showModal('Sensor unbekannt', () => { showModal('Sensor unbekannt', false, () => {
sensorNumberInput.disabled = false; sensorNumberInput.disabled = false;
sensorNumberInput.focus(); sensorNumberInput.focus();
}); });
@@ -69,7 +174,7 @@ async function fetchAddressIfValid() {
console.error('Fehler beim Abrufen der Adresse:', err); console.error('Fehler beim Abrufen der Adresse:', err);
addressInput.value = ''; addressInput.value = '';
sensorNumberInput.disabled = true; sensorNumberInput.disabled = true;
showModal('Sensor unbekannt', () => { showModal('Sensor unbekannt', false, () => {
sensorNumberInput.disabled = false; sensorNumberInput.disabled = false;
sensorNumberInput.focus(); sensorNumberInput.focus();
}); });
@@ -102,8 +207,8 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
} }
try { try {
const url = editId ? `/api/update/${editId}` : '/api/save'; const url = '/api/save';
const method = editId ? 'PUT' : 'POST'; const method = 'POST';
const res = await fetch(url, { const res = await fetch(url, {
method, method,
@@ -115,8 +220,12 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
if (data.error) { if (data.error) {
resultDiv.textContent = data.error; resultDiv.textContent = data.error;
} else { } else {
resultDiv.textContent = editId ? 'Aktualisiert!' : 'Gespeichert!'; resultDiv.textContent = 'OK!';
clearForm(); setTimeout(() => {
resultDiv.textContent = ''
saveBtn.textContent = 'Speichern';
}, 5000)
clearForm(false);
await loadEntries(); await loadEntries();
} }
} catch (err) { } catch (err) {
@@ -125,42 +234,112 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
} }
} }
function clearForm() {
function clearForm(mitButton) {
espIdInput.value = ''; espIdInput.value = '';
sensorNumberInput.value = ''; sensorNumberInput.value = '';
nameInput.value = ''; nameInput.value = '';
descriptionInput.value = ''; descriptionInput.value = '';
addressInput.value = ''; addressInput.value = '';
editId = null; if (mitButton) {
saveBtn.textContent = 'Speichern'; saveBtn.textContent = 'Speichern';
} }
}
const clearUserForm = () => {
document.getElementById('username').value = ''
document.getElementById('password').value = ''
document.getElementById('role').value = 'user'
}
// Globale Sortier-Variable
window.currentSort = window.currentSort || { key: null, asc: true };
async function loadEntries() { async function loadEntries() {
const page = parseInt(pageInput.value) || 1; const page = parseInt(pageInput.value) || 1;
const limit = parseInt(limitInput.value) || 10; const limit = parseInt(limitInput.value) || 50;
try { try {
const res = await fetch(`/api/list?page=${page}&limit=${limit}`); const res = await fetch(`/api/list?page=${page}&limit=${limit}`);
const items = await res.json(); const erg = await res.json();
const items = erg.items
const gz = document.getElementById('gzahl');
gz.innerHTML = `Gesamtzahl: ${erg.anzahl}`
let currentSort = window.currentSort || { key: null, asc: true };
function renderTable(sortedItems) {
tableBody.innerHTML = ''; tableBody.innerHTML = '';
items.forEach(item => { sortedItems.forEach(item => {
const date = new Date(item.createdAt).toISOString().split('T')[0]; const date = new Date(item.chip.lastUpdatedAt).toISOString().split('T')[0];
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.innerHTML = ` tr.innerHTML = `
<td>${item.espId}</td> <td id="tdSensornumber">${item._id}</td>
<td>${item.sensorNumber}</td> <td>${item.chip.id}</td>
<td>${item.name || ''}</td> <td>${item.chip.name || ''}</td>
<td>${item.description || ''}</td> <td id="tdBeschreibung">${item.chip.description || ''}</td>
<td>${item.address || ''}</td> <td id="tdDate">${date}</td>
<td>${date}</td>
<td> <td>
<button data-id="${item._id}" class="editBtn">Bearbeiten</button> <div class="twobuttons">
<button data-id="${item._id}" class="deleteBtn">Löschen</button> <button data-id="${item._id}" class="editBtn" title="Bearbeiten">✏️</button>
<button data-id="${item._id}" class="deleteBtn" title="Löschen">🗑️</button>
</div>
</td> </td>
`; `;
tableBody.appendChild(tr); tableBody.appendChild(tr);
}); });
}
function sortItems(items, key, asc) {
return items.slice().sort((a, b) => {
let valA, valB;
if (key === 'sensorNr') {
valA = a._id;
valB = b._id;
} else if (key === 'espId') {
valA = a.chip.id;
valB = b.chip.id;
} else if (key === 'date') {
valA = new Date(a.chip.lastUpdatedAt);
valB = new Date(b.chip.lastUpdatedAt);
}
if (valA < valB) return asc ? -1 : 1;
if (valA > valB) return asc ? 1 : -1;
return 0;
});
}
// Initial render: Standard nach SensorNr, ESP-ID oder Datum aufsteigend
// Ändere hier die Spalte für die Standardsortierung:
const defaultSortKey = window.currentSort && window.currentSort.key ? window.currentSort.key : 'sensorNr';
const defaultSortAsc = window.currentSort && typeof window.currentSort.asc === 'boolean' ? window.currentSort.asc : true;
currentSort.key = defaultSortKey;
currentSort.asc = defaultSortAsc;
window.currentSort = currentSort;
renderTable(sortItems(items, defaultSortKey, defaultSortAsc));
updateSortArrows();
// Add sort listeners
document.getElementById('thSensorNr').onclick = () => {
currentSort.asc = currentSort.key === 'sensorNr' ? !currentSort.asc : true;
currentSort.key = 'sensorNr';
window.currentSort = currentSort;
renderTable(sortItems(items, 'sensorNr', currentSort.asc));
updateSortArrows();
};
document.getElementById('thEspId').onclick = () => {
currentSort.asc = currentSort.key === 'espId' ? !currentSort.asc : true;
currentSort.key = 'espId';
window.currentSort = currentSort;
renderTable(sortItems(items, 'espId', currentSort.asc));
updateSortArrows();
};
document.getElementById('thDate').onclick = () => {
currentSort.asc = currentSort.key === 'date' ? !currentSort.asc : true;
currentSort.key = 'date';
window.currentSort = currentSort;
renderTable(sortItems(items, 'date', currentSort.asc));
updateSortArrows();
};
document.querySelectorAll('.deleteBtn').forEach(btn => { document.querySelectorAll('.deleteBtn').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
@@ -174,15 +353,25 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
const id = btn.getAttribute('data-id'); const id = btn.getAttribute('data-id');
const res = await fetch(`/api/list?page=1&limit=1&id=${id}`); const res = await fetch(`/api/list?page=1&limit=1&id=${id}`);
const items = await res.json(); const items = await res.json();
const item = items.find(e => e._id === id); const item = items.find(e => e._id === parseInt(id));
if (item) { if (item) {
espIdInput.value = item.espId; espIdInput.value = item.chip.id;
sensorNumberInput.value = item.sensorNumber; sensorNumberInput.value = item._id;
nameInput.value = item.name || ''; nameInput.value = item.chip.name || '';
descriptionInput.value = item.description || ''; descriptionInput.value = item.chip.description || '';
addressInput.value = item.address || ''; addressInput.value = '';
editId = id;
saveBtn.textContent = 'Aktualisieren'; saveBtn.textContent = 'Aktualisieren';
showTab('input')
try {
const rt = await fetch(`api/holAdresse/${item._id}`)
const data = await rt.json();
console.dir(data)
if (!data.error && data.address) {
addressInput.value = data.address;
}
} catch (e) {
console.log("Fehler beim Adresse holen", e)
}
} }
}); });
}); });
@@ -193,21 +382,37 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
} }
async function deleteEntry(id) { async function deleteEntry(id) {
if (!confirm('Wirklich löschen?')) return; showModal('Wirklich löschen?', true, async (confirmed) => {
if (confirmed) {
try { try {
const res = await fetch(`/api/delete/${id}`, { method: 'DELETE' }); const res = await fetch(`/api/delete/${id}`, { method: 'DELETE' });
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
await loadEntries(); await loadEntries();
resultDiv.textContent = 'Eintrag gelöscht.';
setTimeout(() => resultDiv.textContent = '', 3000);
} else {
resultDiv.textContent = 'Fehler beim Löschen.';
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
resultDiv.textContent = 'Fehler beim Löschen.'; resultDiv.textContent = 'Fehler beim Löschen.';
} }
} }
});
}
saveBtn.addEventListener('click', saveEntry); saveBtn.addEventListener('click', saveEntry);
refreshBtn.addEventListener('click', loadEntries); refreshBtn.addEventListener('click', loadEntries);
cancelBtn.addEventListener('click', () => clearForm(true));
userCancelBtn.addEventListener('click', () => clearUserForm(true));
tabInput.addEventListener('click', () => showTab('input'))
tabList.addEventListener('click', () => showTab('list'))
const tabUser = document.getElementById('tabUser');
if (tabUser) tabUser.addEventListener('click', () => showTab('user'))
loadEntries(); loadEntries();
}); });
window.showTab = showTab;

View File

@@ -1,37 +0,0 @@
// public/register.js
document.addEventListener('DOMContentLoaded', () => {
const emailInput = document.getElementById('email');
const emailStatus = document.getElementById('emailStatus');
let debounceTimeout;
emailInput.addEventListener('input', () => {
clearTimeout(debounceTimeout);
const email = emailInput.value.trim();
if (!email) {
emailStatus.textContent = '';
return;
}
// 300ms warten, um zu vermeiden, dass bei jedem Tastendruck eine Anfrage rausgeht
debounceTimeout = setTimeout(async () => {
try {
const res = await fetch(`/api/check-email?email=${encodeURIComponent(email)}`);
const data = await res.json();
if (data.exists) {
emailStatus.textContent = '❌ Diese E-Mail ist schon vergeben';
emailStatus.style.color = 'red';
} else {
emailStatus.textContent = '✅ E-Mail ist frei';
emailStatus.style.color = 'green';
}
} catch (err) {
console.error(err);
emailStatus.textContent = 'Fehler bei der Prüfung';
emailStatus.style.color = 'orange';
}
}, 300);
});
});

View File

@@ -21,6 +21,11 @@
font-weight: bold; font-weight: bold;
box-shadow: 0 2px 8px rgba(0,0,0,0.10); box-shadow: 0 2px 8px rgba(0,0,0,0.10);
} }
#tabUser {
margin-left: 50px;
}
/* Modal Fehlerfenster */ /* Modal Fehlerfenster */
.custom-modal-popup { .custom-modal-popup {
position: fixed; position: fixed;
@@ -95,14 +100,24 @@ input, button {
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-top: 30px;
} }
th, td { th, td {
text-align: left; text-align: left;
padding: 8px; padding: 4px;
border-bottom: 1px solid #eee; border-bottom: 1px solid #888;
} }
/* Spaltenbreiten über colgroup steuern */
col.col-sensornumber { width: 7em; }
col.col-espid {width: 9em}
col.col-bezeichnung { width: 8em; }
col.col-beschreibung{ width: 15em; }
col.col-date { width: 10em; }
col.col-aktionen { width: 2em; }
.controls input#page, .controls input#page,
.controls input#limit { .controls input#limit {
width: 50px; width: 50px;
@@ -151,10 +166,33 @@ button:hover {
background: #0056b3; background: #0056b3;
} }
#saveBtn { .editBtn, .deleteBtn {
margin-top: 20px; background: none;
border: none;
font-size: 18px;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: all 0.2s ease;
margin: 0 2px;
} }
.editBtn:hover {
background: rgba(0, 123, 255, 0.1);
transform: scale(1.1);
}
.deleteBtn:hover {
background: rgba(220, 53, 69, 0.1);
transform: scale(1.1);
}
.twobuttons {
display: flex;
justify-content: space-between;
width: 100%;
gap: 5px;
}
p.error { p.error {
color: red; color: red;
font-weight: bold; font-weight: bold;
@@ -185,3 +223,22 @@ p.error {
min-height: 60px; min-height: 60px;
resize: vertical; resize: vertical;
} }
#gzahl {
margin-left: 30px;
}
#role {
font-size: 12pt;
padding: 5px 0 5px 3px;
margin-bottom: 20px;
}
#version {
width: 100%;
display: flex;
justify-content: flex-end;
font-size: 70%;
color: #007bff;
margin-top: 15px;
}

View File

@@ -1,33 +1,82 @@
import { getCollections } from '../db/mongo.js'; import { MongoClient } from 'mongodb';
import { get_pflux } from '../db/mongo.js';
import { getCollections, update_pflux, clientClose } from '../db/mongo.js';
export function registerAddressRoute(app, requireLogin) { export function registerAddressRoute(app, requireLogin) {
const ADDRESS_SERVICE_URL = process.env.ADDRESS_SERVICE_URL || 'https://noise.fuerst-stuttgart.de/srv/getaddress'; const APIHOST = process.env.APIHOST || 'https://noise.fuerst-stuttgart.de/srv/';
app.get('/api/address/:sensorNumber', requireLogin, async (req, res) => {
const sensorNumber = parseInt(req.params.sensorNumber, 10); const holAdresse = async (id) => {
if (isNaN(sensorNumber)) { // Adresse wie bisher holen (über die Sensornummer via nominative)
return res.status(400).json({ error: 'Ungültige Sensornummer' }); let addressString = '';
} let addrParts = {};
const url = ADDRESS_SERVICE_URL + `?sensorid=${encodeURIComponent(sensorNumber)}`;
try { try {
const url = APIHOST + 'getaddress/' + `?sensorid=${id}`;
console.log(url)
const r = await fetch(url, { headers: { 'Accept': 'application/json' } }); const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
if (!r.ok) { if (r.ok) {
return res.status(502).json({ error: `Adressdienst Fehler (${r.status})` });
}
const data = await r.json(); const data = await r.json();
const addrObj = data?.erg?.address || data?.address || {}; const addrObj = data?.erg?.address || data?.address || {};
const street = addrObj.street ?? ''; const street = addrObj.street ?? '';
const plz = addrObj.plz ?? ''; const plz = addrObj.plz ?? '';
const city = addrObj.city ?? ''; const city = addrObj.city ?? '';
const rightPart = [plz, city].filter(Boolean).join(' ').trim(); const rightPart = [plz, city].filter(Boolean).join(' ').trim();
const addressString = [street, rightPart].filter(Boolean).join(', '); addressString = [street, rightPart].filter(Boolean).join(', ');
return res.json({ addrParts = { street, plz, city };
address: addressString, }
parts: { street, plz, city }
});
} catch (err) { } catch (err) {
console.error('Address lookup failed:', err); console.error('Address lookup failed:', err);
return res.status(504).json({ error: 'Adressdienst nicht erreichbar' });
} }
}); return {
address: addressString,
parts: addrParts,
}
}
app.get('/api/holAdresse/:sensorNumber', requireLogin, async (req, res) => {
const sensorNumber = parseInt(req.params.sensorNumber, 10);
if (isNaN(sensorNumber)) {
return res.status(400).json({ error: 'Ungültige Sensornummer' });
}
const addr = await holAdresse(sensorNumber)
res.json(addr)
})
app.get('/api/address/:sensorNumber', requireLogin, async (req, res) => {
const sensorNumber = parseInt(req.params.sensorNumber, 10);
if (isNaN(sensorNumber)) {
return res.status(400).json({ error: 'Ungültige Sensornummer' });
}
const { propertiesCollection, prop_fluxCollection } = getCollections();
// Suche nach Sensornummer als _id
const propEntry = await propertiesCollection.findOne({ _id: sensorNumber });
if (!propEntry) {
await clientClose()
return res.status(404).json({ error: 'Sensor nicht gefunden' });
}
// Sensor bekannt -> Wurde der schon rüber kopiert?
const propsF = await get_pflux(sensorNumber)
if(!propsF.erg) {
// nein, also kopieren nach prop_flux mit sensorNumber als _id
try {
await prop_fluxCollection.replaceOne(
{ _id: sensorNumber },
{ ...propEntry, _id: sensorNumber },
{ upsert: true }
);
} catch (err) {
console.error('Fehler beim Kopieren nach prop_flux:', err);
// Kein Abbruch, nur Logging
} finally {
await clientClose()
}
}
const adr = await holAdresse(encodeURIComponent(propEntry._id))
adr.props = propsF
return res.json(adr)
})
} }

View File

@@ -1,15 +1,15 @@
import { ObjectId } from 'mongodb'; import { MongoAPIError, ObjectId } from 'mongodb';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { getCollections } from '../db/mongo.js'; import { getCollections, update_pflux } from '../db/mongo.js';
export function registerApiRoutes(app, requireLogin) { export function registerApiRoutes(app, requireLogin) {
const { entriesCollection, usersCollection } = getCollections(); const { usersCollection, prop_fluxCollection } = getCollections();
app.get('/api/check-email', async (req, res) => { app.get('/api/check-email', async (req, res) => {
const email = (req.query.email || '').toLowerCase().trim(); const email = (req.query.email || '').toLowerCase().trim();
if (!email) return res.json({ exists: false }); if (!email) return res.json({ exists: false });
try { try {
const existingUser = await usersCollection.findOne({ email }); const existingUser = await usersCollection.findOne({ email:`${email}` });
res.json({ exists: !!existingUser }); res.json({ exists: !!existingUser });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -18,21 +18,19 @@ export function registerApiRoutes(app, requireLogin) {
}); });
app.post('/api/save', requireLogin, async (req, res) => { app.post('/api/save', requireLogin, async (req, res) => {
let { espId, sensorNumber, name, description, address } = req.body; let { espId, sensorNumber, name, description} = req.body;
if (!espId || !sensorNumber) { if (!espId || !sensorNumber) {
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' }); return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
} }
sensorNumber = parseInt(sensorNumber, 10); sensorNumber = parseInt(sensorNumber, 10);
try { try {
const doc = { const doc = {
espId, id: espId,
sensorNumber,
name: name || '', name: name || '',
description: description || '', description: description || '',
address: address || '', lastUpdatedAt: new Date()
createdAt: new Date()
}; };
await entriesCollection.insertOne(doc); await update_pflux(sensorNumber, doc)
res.json({ success: true }); res.json({ success: true });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -40,30 +38,12 @@ export function registerApiRoutes(app, requireLogin) {
} }
}); });
app.put('/api/update/:id', requireLogin, async (req, res) => {
const { id } = req.params;
let { espId, sensorNumber, name, description, address } = req.body;
if (!espId || !sensorNumber) {
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
}
sensorNumber = parseInt(sensorNumber, 10);
try {
await entriesCollection.updateOne(
{ _id: new ObjectId(id) },
{ $set: { espId, sensorNumber, name, description, address } }
);
res.json({ success: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Fehler beim Aktualisieren' });
}
});
app.get('/api/list', requireLogin, async (req, res) => { app.get('/api/list', requireLogin, async (req, res) => {
const { id } = req.query; const { id } = req.query;
if (id) { if (id) {
try { try {
const item = await entriesCollection.findOne({ _id: new ObjectId(id) }); const item = await prop_fluxCollection.findOne({ _id: parseInt(id) });
if (item) return res.json([item]); if (item) return res.json([item]);
return res.json([]); return res.json([]);
} catch (err) { } catch (err) {
@@ -71,16 +51,24 @@ export function registerApiRoutes(app, requireLogin) {
return res.status(500).json({ error: 'Fehler beim Laden' }); return res.status(500).json({ error: 'Fehler beim Laden' });
} }
} }
let gesamtZahl = 0
try {
gesamtZahl = await prop_fluxCollection.countDocuments({chip: {$exists: true}})
} catch (err) {
console.error(err);
}
const page = parseInt(req.query.page) || 1; const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10; const limit = parseInt(req.query.limit) || 50;
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
try { try {
const items = await entriesCollection.find({}) const items = await prop_fluxCollection.find({chip: {$exists: true}})
.sort({ createdAt: -1 }) .sort({ "chip.lastUpdatedAt": -1 })
.skip(skip) .skip(skip)
.limit(limit) .limit(limit)
.toArray(); .toArray();
res.json(items); const data = {items: items, anzahl: gesamtZahl}
res.json(data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(500).json({ error: 'Fehler beim Laden' }); res.status(500).json({ error: 'Fehler beim Laden' });
@@ -88,7 +76,20 @@ export function registerApiRoutes(app, requireLogin) {
}); });
app.delete('/api/delete/:id', requireLogin, async (req, res) => { app.delete('/api/delete/:id', requireLogin, async (req, res) => {
await entriesCollection.deleteOne({ _id: new ObjectId(req.params.id) }); await prop_fluxCollection.deleteOne({ _id: parseInt(req.params.id) });
res.json({ success: true }); res.json({ success: true });
}); });
app.post('/api/createUser', requireLogin, async (req, res) => {
if (!req.session.isAdmin) return res.status(403).json({ error: 'Nur Admins erlaubt' });
const { username, password, role } = req.body;
if (!username || !password) return res.status(400).json({ error: 'Benutzername und Passwort erforderlich' });
try {
const hash = await bcrypt.hash(password, 10);
await usersCollection.insertOne({ email: username.toLowerCase(), passwordHash: hash, role: role || 'user' });
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: 'Fehler beim Anlegen' });
}
});
} }

View File

@@ -1,30 +1,30 @@
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { getCollections } from '../db/mongo.js'; import { getCollections } from '../db/mongo.js';
import pkg from '../package.json' with { type: "json" }
export function registerAuthRoutes(app) { export function registerAuthRoutes(app) {
const { usersCollection } = getCollections(); const { usersCollection } = getCollections();
const errText = 'Falsche Email oder falsches Passwort.'
app.get('/register', (req, res) => res.render('register', { error: null }));
app.post('/register', async (req, res) => { app.get('/login', (req, res) => {
const { email, password } = req.body; const version = pkg.version
if (!email || !password) return res.render('register', { error: 'Bitte Email und Passwort angeben.' }); const vdate = pkg.date
const existingUser = await usersCollection.findOne({ email: email.toLowerCase() }); res.render('login', {
if (existingUser) return res.render('register', { error: 'Email schon registriert.' }); error: null,
const hash = await bcrypt.hash(password, 10); version: version,
await usersCollection.insertOne({ email: email.toLowerCase(), passwordHash: hash }); vdate: vdate
res.redirect('/login'); })
}); });
app.get('/login', (req, res) => res.render('login', { error: null }));
app.post('/login', async (req, res) => { app.post('/login', async (req, res) => {
const { email, password } = req.body; const { email, password } = req.body;
const user = await usersCollection.findOne({ email: email.toLowerCase() }); const user = await usersCollection.findOne({ email: email.toLowerCase() });
if (!user) return res.render('login', { error: 'Falsche Email oder Passwort.' }); if (!user) return res.render('login', { error: errText });
const match = await bcrypt.compare(password, user.passwordHash); const match = await bcrypt.compare(password, user.passwordHash);
if (!match) return res.render('login', { error: 'Falsche Email oder Passwort.' }); if (!match) return res.render('login', { error: errText });
req.session.userId = user._id; req.session.userId = user._id;
req.session.isAdmin = user.role === 'admin';
res.redirect('/'); res.redirect('/');
}); });

View File

@@ -3,6 +3,7 @@ import session from 'express-session';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import pkg from './package.json' with { type: "json" }
dotenv.config(); dotenv.config();
import { initMongo } from './db/mongo.js'; import { initMongo } from './db/mongo.js';
@@ -36,10 +37,11 @@ await initMongo();
// Login-Middleware // Login-Middleware
function requireLogin(req, res, next) { function requireLogin(req, res, next) {
// if (req.session.userId) return next(); if (req.session.userId) {
// res.redirect('/login');
return next(); return next();
} }
res.redirect('/login');
}
// Routen registrieren // Routen registrieren
registerAuthRoutes(app); registerAuthRoutes(app);
@@ -47,6 +49,11 @@ registerApiRoutes(app, requireLogin);
registerAddressRoute(app, requireLogin); registerAddressRoute(app, requireLogin);
// Hauptseite // Hauptseite
app.get('/', requireLogin, (req, res) => res.render('index')); app.get('/', requireLogin, (req, res) => {
const version = pkg.version
const vdate = pkg.date
const isAdmin = req.session && req.session.isAdmin;
res.render('index', { isAdmin, version, vdate });
});
app.listen(PORT, () => console.log(`Server läuft auf http://localhost:${PORT}`)); app.listen(PORT, () => console.log(`Server läuft auf http://localhost:${PORT}`));

View File

@@ -32,7 +32,7 @@ describe('Server.js API', () => {
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' }); return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
} }
sensorNumber = parseInt(sensorNumber, 10); sensorNumber = parseInt(sensorNumber, 10);
const doc = { espId, sensorNumber, name, description, address, createdAt: new Date(), _id: String(entries.length + 1) }; const doc = { espId, sensorNumber, name, description, address, lastUpdatedAt: new Date(), _id: String(entries.length + 1) };
entries.push(doc); entries.push(doc);
res.json({ success: true }); res.json({ success: true });
}); });
@@ -115,7 +115,7 @@ describe('Server.js API', () => {
}); });
test('PUT /api/update/:id updates entry', async () => { test('PUT /api/update/:id updates entry', async () => {
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() }); entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: new Date() });
const res = await request(app).put('/api/update/1').send({ espId: 'esp2', sensorNumber: '1002', name: 'Neu', description: 'Neu', address: 'Neu' }); const res = await request(app).put('/api/update/1').send({ espId: 'esp2', sensorNumber: '1002', name: 'Neu', description: 'Neu', address: 'Neu' });
expect(res.body).toHaveProperty('success', true); expect(res.body).toHaveProperty('success', true);
expect(entries[0].espId).toBe('esp2'); expect(entries[0].espId).toBe('esp2');
@@ -127,20 +127,20 @@ describe('Server.js API', () => {
}); });
test('GET /api/list returns all entries', async () => { test('GET /api/list returns all entries', async () => {
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() }); entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: new Date() });
const res = await request(app).get('/api/list'); const res = await request(app).get('/api/list');
expect(res.body.length).toBe(1); expect(res.body.length).toBe(1);
}); });
test('GET /api/list?id returns specific entry', async () => { test('GET /api/list?id returns specific entry', async () => {
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() }); entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: new Date() });
const res = await request(app).get('/api/list?id=1'); const res = await request(app).get('/api/list?id=1');
expect(res.body.length).toBe(1); expect(res.body.length).toBe(1);
expect(res.body[0]._id).toBe('1'); expect(res.body[0]._id).toBe('1');
}); });
test('DELETE /api/delete/:id deletes entry', async () => { test('DELETE /api/delete/:id deletes entry', async () => {
entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', createdAt: new Date() }); entries.push({ _id: '1', espId: 'esp1', sensorNumber: 1001, name: '', description: '', address: '', lastUpdatedAt: new Date() });
const res = await request(app).delete('/api/delete/1'); const res = await request(app).delete('/api/delete/1');
expect(res.body).toHaveProperty('success', true); expect(res.body).toHaveProperty('success', true);
expect(entries.length).toBe(0); expect(entries.length).toBe(0);

View File

@@ -11,6 +11,8 @@ html(lang="de")
div.tabs div.tabs
button.tab-btn#tabInput.active(type="button" onclick="showTab('input')") Eingabe button.tab-btn#tabInput.active(type="button" onclick="showTab('input')") Eingabe
button.tab-btn#tabList(type="button" onclick="showTab('list')") Liste button.tab-btn#tabList(type="button" onclick="showTab('list')") Liste
if isAdmin
button.tab-btn#tabUser(type="button" onclick="showTab('user')") User
// Eingabe-Tab // Eingabe-Tab
div#tabInputContent.tab-content div#tabInputContent.tab-content
@@ -29,10 +31,13 @@ html(lang="de")
textarea#description textarea#description
label(for="address") Anschrift: label(for="address") Anschrift:
input#address(type="text" placeholder="Wird automatisch ausgefüllt" readonly) input#address(type="text" placeholder="Wird automatisch ausgefüllt" readonly disabled)
.twobuttons
button#saveBtn(type="button") Speichern button#saveBtn(type="button") Speichern
button#cancelBtn(type="button") Abbrechen
div#result div#result
#version Version: #{version} vom #{vdate}
// Listen-Tab // Listen-Tab
div#tabListContent.tab-content(style="display:none") div#tabListContent.tab-content(style="display:none")
@@ -41,24 +46,45 @@ html(lang="de")
| Seite: | Seite:
input#page(value="1") input#page(value="1")
| Limit: | Limit:
input#limit(value="10") input#limit(value="50")
span#gzahl
table#entriesTable table#entriesTable
colgroup
col.col-sensornumber
col.col-espid
col.col-bezeichnung
col.col-beschreibung
col.col-date
col.col-aktionen
thead thead
tr tr
th SensorNr th(id="thSensorNr" data-sort="sensorNr" style="cursor:pointer") SensorNr <span id="sortArrowSensorNr">↑</span>
th ESP-ID th(id="thEspId" data-sort="espId" style="cursor:pointer") ESP-ID <span id="sortArrowEspId">↑</span>
th Bezeichnung th Bezeichnung
th Beschreibung th Beschreibung
th Anschrift th(id="thDate" data-sort="date" style="cursor:pointer") Datum <span id="sortArrowDate">↑</span>
th Datum
th Aktionen th Aktionen
tbody tbody
// User-Tab (nur für Admins)
if isAdmin
div#tabUserContent.tab-content(style="display:none")
div.card
h2 Neuen User anlegen
form#userForm
label(for="username") Benutzername:
input#username(type="text" required)
label(for="password") Passwort:
input#password(type="password" required)
label(for="role") Rolle:
select#role
option(value="user") User
option(value="admin") Admin
.twobuttons
button#userSaveBtn(type="button") Anlegen
button#userCancelBtn(type="button") Abbrechen
div#userResult
#version Version: #{version} vom #{vdate}
script(type="module" src="/global.js") script(type="module" src="/global.js")
script.
function showTab(tab) {
document.getElementById('tabInputContent').style.display = tab === 'input' ? '' : 'none';
document.getElementById('tabListContent').style.display = tab === 'list' ? '' : 'none';
document.getElementById('tabInput').classList.toggle('active', tab === 'input');
document.getElementById('tabList').classList.toggle('active', tab === 'list');
}

View File

@@ -6,7 +6,9 @@ html(lang="de")
title Login title Login
link(rel="stylesheet", href="/styles.css") link(rel="stylesheet", href="/styles.css")
body body
h1 Login h1 ESP-ID → Sensornummer
div.card
h2 Login
form(method="POST" action="/login") form(method="POST" action="/login")
label(for="email") E-Mail: label(for="email") E-Mail:
input#email(type="email" name="email" required) input#email(type="email" name="email" required)
@@ -14,6 +16,8 @@ html(lang="de")
label(for="password") Passwort: label(for="password") Passwort:
input#password(type="password" name="password" required) input#password(type="password" name="password" required)
button(type="submit") Login button(type="submit") Login
#version Version: #{version} vom #{vdate}
if error if error
p.error= error p.error= error
script(type="module" src="/login.js") script(type="module" src="/login.js")

View File

@@ -1,19 +0,0 @@
doctype html
html(lang="de")
head
meta(charset="utf-8")
meta(name="viewport", content="width=device-width, initial-scale=1")
title Registrieren
link(rel="stylesheet", href="/styles.css")
body
h1 Registrierung
form(method="POST" action="/register")
label(for="email") E-Mail:
input#email(type="email" name="email" required)
span#emailStatus
label(for="password") Passwort:
input#password(type="password" name="password" required)
button(type="submit") Registrieren
if error
p.error= error
script(type="module" src="/register.js")