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
.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 dotenv from 'dotenv';
const MONGO_ROOT_USER = process.env.MONGO_ROOT_USER;
const MONGO_ROOT_PASSWORD = process.env.MONGO_ROOT_PASSWORD;
let MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
dotenv.config();
if (MONGO_ROOT_USER && MONGO_ROOT_PASSWORD) {
const uriParts = MONGO_URI.split('://');
if (uriParts.length === 2) {
const protocol = uriParts[0];
const rest = uriParts[1];
MONGO_URI = `${protocol}://${encodeURIComponent(MONGO_ROOT_USER)}:${encodeURIComponent(MONGO_ROOT_PASSWORD)}@${rest}`;
}
const MONGOHOST = process.env.MONGOHOST || 'localhost'
const MONGOPORT = process.env.MONGOPORT || 27017
const MONGOAUTH = process.env.MONGOAUTH || false
const MONGOUSRP = process.env.MONGOUSRP || ''
const MONGOBASE = process.env.MONGOBASE || 'sensor_data'
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 = process.env.DB_NAME || 'espdb';
const DB_NAME = MONGOBASE
let db, entriesCollection, usersCollection;
let db, usersCollection, prop_fluxCollection, propertiesCollection;
let client = null;
export async function initMongo() {
const client = new MongoClient(MONGO_URI);
const client = new MongoClient(MONGO_URL);
await client.connect();
db = client.db(DB_NAME);
entriesCollection = db.collection('entries');
usersCollection = db.collection('users');
return { db, entriesCollection, usersCollection };
usersCollection = db.collection('user');
prop_fluxCollection = db.collection('prop_flux');
propertiesCollection = db.collection('properties')
return { db, usersCollection, prop_fluxCollection, propertiesCollection};
}
export function getCollections() {
return { db, entriesCollection, usersCollection };
export const clientClose = async () => {
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:
app:
build: .
container_name: esp-app
container_name: esp2sensor
ports:
- "3000:3000"
environment:
- PORT=3000
- MONGO_URI=mongodb://mongo:27017
- DB_NAME=espdb
depends_on:
- mongo
volumes:
- .:/app # bind mount für Live-Reload
- /app/node_modules # node_modules vom Host nicht überschreiben
- ./log:/var/log
restart: unless-stopped
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",
"version": "1.0.0",
"version": "1.2.0",
"date": "2025-09-02 17:00 UTC",
"type": "module",
"description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB",
"main": "server.js",
"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",
"test": "jest"
},
@@ -13,14 +14,14 @@
"bcrypt": "^6.0.0",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^4.18.2",
"express": "^5.1.0",
"express-session": "^1.18.2",
"mongodb": "^6.6.0",
"pug": "^3.0.2"
"mongodb": "^6.19.0",
"pug": "^3.0.3"
},
"devDependencies": {
"jest": "^30.0.5",
"nodemon": "^3.0.1",
"jest": "^30.1.3",
"nodemon": "^3.1.10",
"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', () => {
const saveBtn = document.getElementById('saveBtn');
const refreshBtn = document.getElementById('refreshBtn');
@@ -10,12 +72,13 @@ document.addEventListener('DOMContentLoaded', () => {
const limitInput = document.getElementById('limit');
const resultDiv = document.getElementById('result');
const tableBody = document.querySelector('#entriesTable tbody');
const tabInput = document.getElementById('tabInput');
const tabList = document.getElementById('tabList');
let editId = null;
// Modal für Fehleranzeige
function showModal(message, callback) {
// Vorherige Modals entfernen
function showModal(message, showCancelButton, callback) {
// Remove previous modals
document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove());
let modal = document.createElement('div');
@@ -29,38 +92,80 @@ document.addEventListener('DOMContentLoaded', () => {
msg.textContent = message;
box.appendChild(msg);
let btn = document.createElement('button');
btn.className = 'custom-modal-btn';
btn.textContent = 'OK';
btn.onclick = () => {
let btndiv = document.createElement('div')
btndiv.className = 'twobuttons'
// 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) {
modal.parentNode.removeChild(modal);
}
if (callback) callback();
if (callback) callback(false); // Pass false for Cancel
};
box.appendChild(btn);
modal.appendChild(box);
document.body.appendChild(modal);
btndiv.appendChild(btnCancel);
}
// Sensornummer nur Zahlen erlauben
sensorNumberInput.addEventListener('input', () => {
sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, '');
});
// 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
};
// Adresse vom Server holen, wenn Enter oder Feld verlassen
async function fetchAddressIfValid() {
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
sensorNumberInput.addEventListener('input', () => {
sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, '');
});
// Adresse vom Server holen, wenn Enter oder Feld verlassen
async function fetchAddressIfValid() {
const value = sensorNumberInput.value.trim();
if (value.length > 0) {
try {
const res = await fetch(`/api/address/${value}`);
const data = await res.json();
console.dir(data)
if (!data.error && 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 {
addressInput.value = '';
sensorNumberInput.disabled = true;
showModal('Sensor unbekannt', () => {
showModal('Sensor unbekannt', false, () => {
sensorNumberInput.disabled = false;
sensorNumberInput.focus();
});
@@ -69,24 +174,24 @@ async function fetchAddressIfValid() {
console.error('Fehler beim Abrufen der Adresse:', err);
addressInput.value = '';
sensorNumberInput.disabled = true;
showModal('Sensor unbekannt', () => {
showModal('Sensor unbekannt', false, () => {
sensorNumberInput.disabled = false;
sensorNumberInput.focus();
});
}
}
}
}
// Enter-Taste
sensorNumberInput.addEventListener('keydown', (e) => {
// Enter-Taste
sensorNumberInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
fetchAddressIfValid();
}
});
});
// Feld verlassen
sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
// Feld verlassen
sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
async function saveEntry() {
@@ -102,8 +207,8 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
}
try {
const url = editId ? `/api/update/${editId}` : '/api/save';
const method = editId ? 'PUT' : 'POST';
const url = '/api/save';
const method = 'POST';
const res = await fetch(url, {
method,
@@ -115,8 +220,12 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
if (data.error) {
resultDiv.textContent = data.error;
} else {
resultDiv.textContent = editId ? 'Aktualisiert!' : 'Gespeichert!';
clearForm();
resultDiv.textContent = 'OK!';
setTimeout(() => {
resultDiv.textContent = ''
saveBtn.textContent = 'Speichern';
}, 5000)
clearForm(false);
await loadEntries();
}
} catch (err) {
@@ -125,42 +234,112 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
}
}
function clearForm() {
function clearForm(mitButton) {
espIdInput.value = '';
sensorNumberInput.value = '';
nameInput.value = '';
descriptionInput.value = '';
addressInput.value = '';
editId = null;
if (mitButton) {
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() {
const page = parseInt(pageInput.value) || 1;
const limit = parseInt(limitInput.value) || 10;
const limit = parseInt(limitInput.value) || 50;
try {
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 = '';
items.forEach(item => {
const date = new Date(item.createdAt).toISOString().split('T')[0];
sortedItems.forEach(item => {
const date = new Date(item.chip.lastUpdatedAt).toISOString().split('T')[0];
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${item.espId}</td>
<td>${item.sensorNumber}</td>
<td>${item.name || ''}</td>
<td>${item.description || ''}</td>
<td>${item.address || ''}</td>
<td>${date}</td>
<td id="tdSensornumber">${item._id}</td>
<td>${item.chip.id}</td>
<td>${item.chip.name || ''}</td>
<td id="tdBeschreibung">${item.chip.description || ''}</td>
<td id="tdDate">${date}</td>
<td>
<button data-id="${item._id}" class="editBtn">Bearbeiten</button>
<button data-id="${item._id}" class="deleteBtn">Löschen</button>
<div class="twobuttons">
<button data-id="${item._id}" class="editBtn" title="Bearbeiten">✏️</button>
<button data-id="${item._id}" class="deleteBtn" title="Löschen">🗑️</button>
</div>
</td>
`;
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 => {
btn.addEventListener('click', async () => {
@@ -174,15 +353,25 @@ sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
const id = btn.getAttribute('data-id');
const res = await fetch(`/api/list?page=1&limit=1&id=${id}`);
const items = await res.json();
const item = items.find(e => e._id === id);
const item = items.find(e => e._id === parseInt(id));
if (item) {
espIdInput.value = item.espId;
sensorNumberInput.value = item.sensorNumber;
nameInput.value = item.name || '';
descriptionInput.value = item.description || '';
addressInput.value = item.address || '';
editId = id;
espIdInput.value = item.chip.id;
sensorNumberInput.value = item._id;
nameInput.value = item.chip.name || '';
descriptionInput.value = item.chip.description || '';
addressInput.value = '';
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) {
if (!confirm('Wirklich löschen?')) return;
showModal('Wirklich löschen?', true, async (confirmed) => {
if (confirmed) {
try {
const res = await fetch(`/api/delete/${id}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
await loadEntries();
resultDiv.textContent = 'Eintrag gelöscht.';
setTimeout(() => resultDiv.textContent = '', 3000);
} else {
resultDiv.textContent = 'Fehler beim Löschen.';
}
} catch (err) {
console.error(err);
resultDiv.textContent = 'Fehler beim Löschen.';
}
}
});
}
saveBtn.addEventListener('click', saveEntry);
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();
});
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;
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
}
#tabUser {
margin-left: 50px;
}
/* Modal Fehlerfenster */
.custom-modal-popup {
position: fixed;
@@ -95,14 +100,24 @@ input, button {
table {
width: 100%;
border-collapse: collapse;
margin-top: 30px;
}
th, td {
text-align: left;
padding: 8px;
border-bottom: 1px solid #eee;
padding: 4px;
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#limit {
width: 50px;
@@ -151,10 +166,33 @@ button:hover {
background: #0056b3;
}
#saveBtn {
margin-top: 20px;
.editBtn, .deleteBtn {
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 {
color: red;
font-weight: bold;
@@ -185,3 +223,22 @@ p.error {
min-height: 60px;
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) {
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);
if (isNaN(sensorNumber)) {
return res.status(400).json({ error: 'Ungültige Sensornummer' });
}
const url = ADDRESS_SERVICE_URL + `?sensorid=${encodeURIComponent(sensorNumber)}`;
const holAdresse = async (id) => {
// Adresse wie bisher holen (über die Sensornummer via nominative)
let addressString = '';
let addrParts = {};
try {
const url = APIHOST + 'getaddress/' + `?sensorid=${id}`;
console.log(url)
const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
if (!r.ok) {
return res.status(502).json({ error: `Adressdienst Fehler (${r.status})` });
}
if (r.ok) {
const data = await r.json();
const addrObj = data?.erg?.address || data?.address || {};
const street = addrObj.street ?? '';
const plz = addrObj.plz ?? '';
const city = addrObj.city ?? '';
const rightPart = [plz, city].filter(Boolean).join(' ').trim();
const addressString = [street, rightPart].filter(Boolean).join(', ');
return res.json({
address: addressString,
parts: { street, plz, city }
});
addressString = [street, rightPart].filter(Boolean).join(', ');
addrParts = { street, plz, city };
}
} catch (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 { getCollections } from '../db/mongo.js';
import { getCollections, update_pflux } from '../db/mongo.js';
export function registerApiRoutes(app, requireLogin) {
const { entriesCollection, usersCollection } = getCollections();
const { usersCollection, prop_fluxCollection } = getCollections();
app.get('/api/check-email', async (req, res) => {
const email = (req.query.email || '').toLowerCase().trim();
if (!email) return res.json({ exists: false });
try {
const existingUser = await usersCollection.findOne({ email });
const existingUser = await usersCollection.findOne({ email:`${email}` });
res.json({ exists: !!existingUser });
} catch (err) {
console.error(err);
@@ -18,21 +18,19 @@ export function registerApiRoutes(app, requireLogin) {
});
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) {
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
}
sensorNumber = parseInt(sensorNumber, 10);
try {
const doc = {
espId,
sensorNumber,
id: espId,
name: name || '',
description: description || '',
address: address || '',
createdAt: new Date()
lastUpdatedAt: new Date()
};
await entriesCollection.insertOne(doc);
await update_pflux(sensorNumber, doc)
res.json({ success: true });
} catch (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) => {
const { id } = req.query;
if (id) {
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]);
return res.json([]);
} catch (err) {
@@ -71,16 +51,24 @@ export function registerApiRoutes(app, requireLogin) {
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 limit = parseInt(req.query.limit) || 10;
const limit = parseInt(req.query.limit) || 50;
const skip = (page - 1) * limit;
try {
const items = await entriesCollection.find({})
.sort({ createdAt: -1 })
const items = await prop_fluxCollection.find({chip: {$exists: true}})
.sort({ "chip.lastUpdatedAt": -1 })
.skip(skip)
.limit(limit)
.toArray();
res.json(items);
const data = {items: items, anzahl: gesamtZahl}
res.json(data);
} catch (err) {
console.error(err);
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) => {
await entriesCollection.deleteOne({ _id: new ObjectId(req.params.id) });
await prop_fluxCollection.deleteOne({ _id: parseInt(req.params.id) });
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 { getCollections } from '../db/mongo.js';
import pkg from '../package.json' with { type: "json" }
export function registerAuthRoutes(app) {
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) => {
const { email, password } = req.body;
if (!email || !password) return res.render('register', { error: 'Bitte Email und Passwort angeben.' });
const existingUser = await usersCollection.findOne({ email: email.toLowerCase() });
if (existingUser) return res.render('register', { error: 'Email schon registriert.' });
const hash = await bcrypt.hash(password, 10);
await usersCollection.insertOne({ email: email.toLowerCase(), passwordHash: hash });
res.redirect('/login');
app.get('/login', (req, res) => {
const version = pkg.version
const vdate = pkg.date
res.render('login', {
error: null,
version: version,
vdate: vdate
})
});
app.get('/login', (req, res) => res.render('login', { error: null }));
app.post('/login', async (req, res) => {
const { email, password } = req.body;
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);
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.isAdmin = user.role === 'admin';
res.redirect('/');
});

View File

@@ -3,6 +3,7 @@ import session from 'express-session';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
import pkg from './package.json' with { type: "json" }
dotenv.config();
import { initMongo } from './db/mongo.js';
@@ -36,9 +37,10 @@ await initMongo();
// Login-Middleware
function requireLogin(req, res, next) {
// if (req.session.userId) return next();
// res.redirect('/login');
if (req.session.userId) {
return next();
}
res.redirect('/login');
}
// Routen registrieren
@@ -47,6 +49,11 @@ registerApiRoutes(app, requireLogin);
registerAddressRoute(app, requireLogin);
// 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}`));

View File

@@ -32,7 +32,7 @@ describe('Server.js API', () => {
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
}
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);
res.json({ success: true });
});
@@ -115,7 +115,7 @@ describe('Server.js API', () => {
});
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' });
expect(res.body).toHaveProperty('success', true);
expect(entries[0].espId).toBe('esp2');
@@ -127,20 +127,20 @@ describe('Server.js API', () => {
});
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');
expect(res.body.length).toBe(1);
});
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');
expect(res.body.length).toBe(1);
expect(res.body[0]._id).toBe('1');
});
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');
expect(res.body).toHaveProperty('success', true);
expect(entries.length).toBe(0);

View File

@@ -11,6 +11,8 @@ html(lang="de")
div.tabs
button.tab-btn#tabInput.active(type="button" onclick="showTab('input')") Eingabe
button.tab-btn#tabList(type="button" onclick="showTab('list')") Liste
if isAdmin
button.tab-btn#tabUser(type="button" onclick="showTab('user')") User
// Eingabe-Tab
div#tabInputContent.tab-content
@@ -29,10 +31,13 @@ html(lang="de")
textarea#description
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#cancelBtn(type="button") Abbrechen
div#result
#version Version: #{version} vom #{vdate}
// Listen-Tab
div#tabListContent.tab-content(style="display:none")
@@ -41,24 +46,45 @@ html(lang="de")
| Seite:
input#page(value="1")
| Limit:
input#limit(value="10")
input#limit(value="50")
span#gzahl
table#entriesTable
colgroup
col.col-sensornumber
col.col-espid
col.col-bezeichnung
col.col-beschreibung
col.col-date
col.col-aktionen
thead
tr
th SensorNr
th ESP-ID
th(id="thSensorNr" data-sort="sensorNr" style="cursor:pointer") SensorNr <span id="sortArrowSensorNr">↑</span>
th(id="thEspId" data-sort="espId" style="cursor:pointer") ESP-ID <span id="sortArrowEspId">↑</span>
th Bezeichnung
th Beschreibung
th Anschrift
th Datum
th(id="thDate" data-sort="date" style="cursor:pointer") Datum <span id="sortArrowDate">↑</span>
th Aktionen
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.
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
link(rel="stylesheet", href="/styles.css")
body
h1 Login
h1 ESP-ID → Sensornummer
div.card
h2 Login
form(method="POST" action="/login")
label(for="email") E-Mail:
input#email(type="email" name="email" required)
@@ -14,6 +16,8 @@ html(lang="de")
label(for="password") Passwort:
input#password(type="password" name="password" required)
button(type="submit") Login
#version Version: #{version} vom #{vdate}
if error
p.error= error
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")