Compare commits
30 Commits
b2f6fa0993
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b1b8f39ff4 | |||
| c0fe05c67e | |||
| 2e5a7be4dd | |||
| ef12b275f5 | |||
| 782055c4fd | |||
| ab74164608 | |||
| bc9976d5c6 | |||
| 0c45cd9f46 | |||
| ddc503cb63 | |||
| 19c6facf71 | |||
| 278f91ed3c | |||
| 7b3403d943 | |||
| 6ff2ceb76d | |||
| cd4d7ba25d | |||
| 47d27148ca | |||
| ccaf6dd6f3 | |||
| 4283f07291 | |||
| b8bcd946d0 | |||
| b92dffef0f | |||
| 0d10e17caf | |||
| cf55afda38 | |||
| 306239cfbc | |||
| b5ffd8ae4a | |||
| add666b297 | |||
| 8891addccb | |||
| ff789a68ef | |||
| 1c213634c7 | |||
| f4bf4fffd6 | |||
| d7a9a7a487 | |||
| d081b9f52d |
@@ -6,3 +6,6 @@ log/
|
||||
*.log
|
||||
.env
|
||||
**/.env
|
||||
|
||||
# Produktionsstand vom Server, nur Referenz
|
||||
noisesensors/vonRemote/
|
||||
|
||||
@@ -34,7 +34,7 @@ Ablage für die Logdateien der einzelnene Container (**noise.log**, **sensorapi.
|
||||
hier darunter befinden sich alle Daten, sowohl die Datenbanken als auch Konfigurationen. Außerdem werden jeweils die laufend eingelesenen Daten als **actdata.json** abgelegt. Diese werden bei jedem neuen Einlesen wieder überschrieben.
|
||||
* **data/mongo**
|
||||
Verzeichnis für die Mongo-Datenbank
|
||||
* **data/mngo/data**
|
||||
* **data/mongo/data**
|
||||
hier befinden sich die eingelesenen Daten
|
||||
* **data/mongo/entries**
|
||||
hier liegt die Datei **create.js**, mit deren Hilfe bei ersten Start der Datenbank die Collections und die Indexe erzeugt werden.
|
||||
@@ -45,7 +45,7 @@ Hier dann die eingelesenen Daten
|
||||
|
||||
|
||||
### Aufrufe
|
||||
Entpacken des Tar-Files **noise.tgz** in einem beliebigen Verzeichnis (das Direktoty *noisesensors* wird angelegt). Der komplette Pfad zu dem Verzeichnis (also ab root) muss in der Environmentvariablen **LOCALDIR** hinterlegt werden.
|
||||
Entpacken des Tar-Files **noise.tgz** in einem beliebigen Verzeichnis (das Verzeichnis *noisesensors* wird angelegt). Der komplette Pfad zu dem Verzeichnis (also ab root) muss in der Environmentvariablen **LOCALDIR** hinterlegt werden.
|
||||
|
||||
Nun das *docker-compose.yml* in das Portainer-Stack einfügen. Zusätzlich folgende Environment-Variablen festlegen:
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.vscode
|
||||
docs
|
||||
node_modules
|
||||
tests
|
||||
.gitignore
|
||||
build_and_copy
|
||||
deploy.sh
|
||||
docker-compose.yml
|
||||
Dockerfile_esp2sensor
|
||||
hashpasswd
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
log/
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.env
|
||||
*.pdf
|
||||
.DS_Store
|
||||
._*
|
||||
docs/beschreibung_tmp.html
|
||||
docs/beschreibung.html
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"yzane.markdown-pdf"
|
||||
]
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/server.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
ADD package.json /tmp/package.json
|
||||
ADD package-lock.json /tmp/package-lock.json
|
||||
RUN cd /tmp && npm ci
|
||||
RUN mkdir -p /opt/app && cp -a /tmp/node_modules /tmp/package.json /opt/app/
|
||||
WORKDIR /opt/app
|
||||
ADD . /opt/app/
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
ENV TZ=Europe/Berlin
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
RUN deluser --remove-home node
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
@@ -0,0 +1,59 @@
|
||||
import { MongoClient } from 'mongodb';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
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 = MONGOBASE
|
||||
|
||||
let db, usersCollection, propCollection;
|
||||
let client = null;
|
||||
|
||||
export async function initMongo() {
|
||||
const client = new MongoClient(MONGO_URL);
|
||||
await client.connect();
|
||||
db = client.db(DB_NAME);
|
||||
usersCollection = db.collection('user');
|
||||
propCollection = db.collection('properties')
|
||||
return { db, usersCollection, propCollection};
|
||||
}
|
||||
|
||||
export const clientClose = async () => {
|
||||
if (client) {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function getCollections() {
|
||||
return { db, usersCollection, propCollection};
|
||||
}
|
||||
|
||||
export const update_pflux = async(sn, doc) => {
|
||||
try {
|
||||
await propCollection.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 propCollection.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}
|
||||
}
|
||||
}
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Angepasst aus dem noise-/sensorapi-deploy.sh
|
||||
# Nur linux/amd64, nativer Build auf esprimo (amd64) -> kein buildx/qemu,
|
||||
# eigener Dockerfile-Name (Dockerfile_esp2sensor).
|
||||
|
||||
# Ziel-Registry, per Umgebungsvariable ueberschreibbar:
|
||||
# REGISTRY=anderes.registry.example ./deploy.sh
|
||||
REGISTRY="${REGISTRY:-ghcr.io/dnms-org/laermkarte-sensor-community}"
|
||||
IMAGE_NAME="esp2sensor"
|
||||
TAG="${1:-latest}"
|
||||
PLATFORM="linux/amd64"
|
||||
DOCKERFILE="Dockerfile_esp2sensor"
|
||||
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
BUILD_DATE=$(date +%d.%m.%Y)
|
||||
|
||||
echo "=========================================="
|
||||
echo "esp2sensor Deploy Script (amd64)"
|
||||
echo "=========================================="
|
||||
echo "Registry: ${REGISTRY}"
|
||||
echo "Image: ${IMAGE_NAME}"
|
||||
echo "Tag: ${TAG}"
|
||||
echo "Platform: ${PLATFORM}"
|
||||
echo "Dockerfile: ${DOCKERFILE}"
|
||||
echo "Build-Datum: ${BUILD_DATE}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Hinweis: Login wird uebersprungen, weil die Credentials fuer ${REGISTRY}
|
||||
# bereits in ~/.docker/config.json liegen.
|
||||
# Bei Bedarf manuell: docker login ${REGISTRY}
|
||||
|
||||
echo ">>> Baue Image (nur ${PLATFORM}, nativ) ..."
|
||||
docker build \
|
||||
--platform "${PLATFORM}" \
|
||||
-f "${DOCKERFILE}" \
|
||||
-t "${FULL_IMAGE}" \
|
||||
.
|
||||
echo ""
|
||||
|
||||
echo ">>> Pushe ${FULL_IMAGE} ..."
|
||||
docker push "${FULL_IMAGE}"
|
||||
echo ""
|
||||
|
||||
# Falls ein anderer Tag als 'latest' gebaut wurde, zusaetzlich latest setzen+pushen
|
||||
if [ "${TAG}" != "latest" ]; then
|
||||
echo ">>> Tagge zusaetzlich als :latest ..."
|
||||
docker tag "${FULL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest"
|
||||
docker push "${REGISTRY}/${IMAGE_NAME}:latest"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Build + Push fertig: ${FULL_IMAGE}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Danach Stack aktualisieren:"
|
||||
echo " cd /opt/stacks/esprimo/noisesensors && docker compose up -d esp2sensor"
|
||||
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: esp2sensor
|
||||
ports:
|
||||
- "3000:3000"
|
||||
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:
|
||||
image: mongo:6
|
||||
container_name: esp-mongo
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
|
||||
volumes:
|
||||
mongo-data:
|
||||
@@ -0,0 +1,65 @@
|
||||
# Bedienungsanleitung esp2sensor
|
||||
|
||||
Diese Anleitung richtet sich an die Anwender der Webapp. Technische Hintergründe (Deployment, Erststart/Bootstrap) stehen in `beschreibung.md`.
|
||||
|
||||
|
||||
## Anmeldung
|
||||
|
||||
Als Benutzername dient die **E-Mail-Adresse**. Während der Eingabe wird geprüft, ob diese bekannt ist; solange die Eingabe unvollständig ist, erscheint *Benutzer nicht gefunden*.
|
||||
|
||||
Passwort vergessen? Bitte an einen Admin wenden — der kann es über den **User**-Tab zurücksetzen.
|
||||
|
||||
Nach erfolgreicher Anmeldung gelangt man zur Hauptseite mit den Tabs am oberen Rand. Welche Tabs sichtbar sind, hängt davon ab, ob man als **Admin** oder als normaler **User** angemeldet ist. Ganz rechts in der Tab-Zeile befindet sich der **Logout**-Button, mit dem man sich jederzeit abmelden kann.
|
||||
|
||||
|
||||
## Tab „Eingabe“
|
||||
|
||||
Dieser Tab ist nach der Anmeldung standardmäßig ausgewählt.
|
||||
|
||||
* In die Zeile **Sensornummer** wird die Sensornummer (von sensor.community) eingetragen. Ist sie bereits bekannt, wird die zugehörige Adresse automatisch in der Zeile **Anschrift** angezeigt. Ist sie unbekannt, erscheint eine Fehlermeldung.
|
||||
* Danach können **ESP-ID**, **Bezeichnung** und **Beschreibung** eingegeben werden. Nur die ESP-ID ist Pflicht, die anderen beiden Felder sind optional. Die Anschrift kann nicht manuell verändert werden, sie ergibt sich immer aus der Sensornummer.
|
||||
* Über **Speichern** werden die Daten übernommen. Bereits vorhandene Daten zu dieser Sensornummer werden dabei überschrieben.
|
||||
* Wurde die Sensornummer bereits einmal erfasst, werden die vorhandenen Daten automatisch geladen und können hier geändert werden.
|
||||
|
||||
|
||||
## Tab „Liste“
|
||||
|
||||
Zeigt alle erfassten Datensätze. Über **Aktualisieren** wird die Liste neu geladen, über **Seite** und **Limit** lässt sich blättern bzw. die Anzahl der Einträge pro Seite einstellen.
|
||||
|
||||
In der Spalte **Aktionen** gibt es zwei Symbole:
|
||||
* **Stift** (✏️) — lädt den Eintrag zur Bearbeitung in den Eingabe-Tab.
|
||||
* **Mülleimer** (🗑️) — löscht den Eintrag nach einer Sicherheitsrückfrage.
|
||||
|
||||
Die Liste lässt sich nach **SensorNr**, **ESP-ID** und **Datum** auf- oder absteigend sortieren (Pfeile ↑↓ in der Kopfzeile).
|
||||
|
||||
|
||||
## Tab „Profil“
|
||||
|
||||
Hier kann jeder angemeldete User sein eigenes Passwort ändern:
|
||||
|
||||
* **Aktuelles Passwort**
|
||||
* **Neues Passwort**
|
||||
* **Neues Passwort (Wiederholung)**
|
||||
|
||||
Über **Ändern** wird das neue Passwort gespeichert. Stimmen die beiden neuen Passwörter nicht überein oder ist das aktuelle Passwort falsch, erscheint eine entsprechende Fehlermeldung.
|
||||
|
||||
|
||||
## Tab „User“ (nur für Admins)
|
||||
|
||||
Dieser Tab ist nur sichtbar, wenn man als Admin angemeldet ist.
|
||||
|
||||
**Neuen User anlegen:** Benutzername (E-Mail), Passwort und Rolle (*User* oder *Admin*) eingeben und auf **Anlegen** klicken.
|
||||
|
||||
**Angemeldete User:** darunter erscheint eine Liste aller vorhandenen User mit E-Mail und Rolle. Pro Zeile stehen zwei Aktionen zur Verfügung:
|
||||
* **Passwort zurücksetzen** (🔑) — öffnet ein Eingabefeld für ein neues Passwort für diesen User.
|
||||
* **User löschen** (🗑️) — löscht den User nach einer Sicherheitsrückfrage. Der eigene Account kann nicht gelöscht werden.
|
||||
|
||||
|
||||
## Abmelden
|
||||
|
||||
Über den **Logout**-Button rechts in der Tab-Zeile wird die Sitzung beendet und man gelangt zurück zur Login-Seite.
|
||||
|
||||
### History
|
||||
| Version | Datum | Author |Bemerkung|
|
||||
|---------|-------|-----|----------|
|
||||
| 1.0.0 | 2026-08-07 | rxf | erste Fassung
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
Executable
+30
@@ -0,0 +1,30 @@
|
||||
# Beschreibung von esp2senor
|
||||
|
||||
|
||||
### Erststart / Bootstrap
|
||||
|
||||
Ist die `user`-Collection in der Mongo-Datenbank leer (z.B. bei einem neuen, extern verwalteten Deployment ohne direkten Datenbankzugriff), legt der Server beim Start automatisch einen Admin-User an — vorausgesetzt, die Umgebungsvariablen `ADMIN_EMAIL` und `ADMIN_PASSWORD` sind gesetzt. Fehlen sie, erscheint auf der Login-Seite ein entsprechender Hinweis und im Log eine Warnung.
|
||||
|
||||
Nach dem ersten Login mit diesen Zugangsdaten sollte das Passwort über den **Profil**-Tab geändert werden. Sobald mindestens ein User existiert, greift der Bootstrap-Mechanismus nicht mehr — weitere User werden wie gewohnt über den **User**-Tab (nur für Admins) angelegt.
|
||||
|
||||
|
||||
### Darstellung
|
||||
|
||||
Die Login-Seite dient der Anmeldung per E-Mail und Passwort. Nach erfolgreicher Anmeldung erscheint die Hauptseite mit den Tabs **Eingabe**, **Liste**, **Profil** und — nur für Admins — **User**, sowie einem **Logout**-Button ganz rechts in der Tab-Zeile.
|
||||
|
||||
* **Eingabe**: Erfassen/Ändern eines Datensatzes über Sensornummer (mit automatischer Adressermittlung), ESP-ID, Bezeichnung und Beschreibung.
|
||||
* **Liste**: paginierte, sortierbare Übersicht aller Datensätze mit Bearbeiten- und Löschen-Aktion je Zeile.
|
||||
* **Profil**: Ändern des eigenen Passworts (aktuelles Passwort, neues Passwort, Wiederholung).
|
||||
* **User** (nur Admin): Anlegen neuer User (E-Mail, Passwort, Rolle) sowie eine Liste aller vorhandenen User mit Aktionen zum Passwort-Zurücksetzen und Löschen (der eigene Account kann nicht gelöscht werden).
|
||||
|
||||
Eine ausführliche Bedienungsanleitung für Endanwender steht in `Bedienungsanleitung.md`.
|
||||
|
||||
|
||||
#### History
|
||||
| Version | Datum | Author |Bemerkung|
|
||||
|---------|-------|-----|----------|
|
||||
| 1.2.0 | 2026-08-07 | rxf | verkürzt auf technische Dinge, Verweis auf Bedienunsgaleitung
|
||||
| 1.1.0 | 2025-09-03 | rxf | Ändern,Löschen und Usereingabe |
|
||||
|1.0.0 | 2025-08-19 | rxf | erste Fassung |
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
const hashedPassword = await bcrypt.hash('Tux4esp', 10);
|
||||
console.log(hashedPassword)
|
||||
Generated
+6549
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "espid2sensor",
|
||||
"version": "1.6.3",
|
||||
"date": "2026-08-07",
|
||||
"type": "module",
|
||||
"description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js >>/var/log/esp2sensor.log 2>&1",
|
||||
"dev": "nodemon --watch server.js --watch views --watch public server.js",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^5.1.0",
|
||||
"express-session": "^1.18.2",
|
||||
"mongodb": "^6.19.0",
|
||||
"pug": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^30.1.3",
|
||||
"nodemon": "^3.1.10",
|
||||
"supertest": "^7.1.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
// 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';
|
||||
document.getElementById('tabProfileContent').style.display = tab === 'profile' ? '' : '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');
|
||||
document.getElementById('tabProfile').classList.toggle('active', tab === 'profile');
|
||||
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!';
|
||||
if (window.loadUsers) window.loadUsers();
|
||||
} else {
|
||||
userResult.textContent = data.error || 'Fehler beim Anlegen.';
|
||||
}
|
||||
} catch (err) {
|
||||
userResult.textContent = 'Serverfehler.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const profileSaveBtn = document.getElementById('profileSaveBtn');
|
||||
if (profileSaveBtn) {
|
||||
profileSaveBtn.addEventListener('click', async () => {
|
||||
const currentPassword = document.getElementById('currentPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const newPasswordRepeat = document.getElementById('newPasswordRepeat').value;
|
||||
const profileResult = document.getElementById('profileResult');
|
||||
if (!currentPassword || !newPassword) {
|
||||
profileResult.textContent = 'Aktuelles und neues Passwort erforderlich.';
|
||||
return;
|
||||
}
|
||||
if (newPassword !== newPasswordRepeat) {
|
||||
profileResult.textContent = 'Die Wiederholung stimmt nicht mit dem neuen Passwort überein.';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/changePassword', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ currentPassword, newPassword })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
profileResult.textContent = 'Passwort erfolgreich geändert!';
|
||||
document.getElementById('profileForm').reset();
|
||||
} else {
|
||||
profileResult.textContent = data.error || 'Fehler beim Ändern.';
|
||||
}
|
||||
} catch (err) {
|
||||
profileResult.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');
|
||||
const espIdInput = document.getElementById('espId');
|
||||
const sensorNumberInput = document.getElementById('sensorNumber');
|
||||
const nameInput = document.getElementById('name');
|
||||
const descriptionInput = document.getElementById('description');
|
||||
const addressInput = document.getElementById('address');
|
||||
const pageInput = document.getElementById('page');
|
||||
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');
|
||||
const cancelBtn = document.getElementById('cancelBtn');
|
||||
const userCancelBtn = document.getElementById('userCancelBtn');
|
||||
|
||||
|
||||
// Modal für Fehleranzeige
|
||||
function showModal(message, showCancelButton, callback) {
|
||||
// Remove previous modals
|
||||
document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove());
|
||||
|
||||
let modal = document.createElement('div');
|
||||
modal.className = 'custom-modal-popup';
|
||||
|
||||
let box = document.createElement('div');
|
||||
box.className = 'custom-modal-box';
|
||||
|
||||
let msg = document.createElement('div');
|
||||
msg.className = 'custom-modal-msg';
|
||||
msg.textContent = message;
|
||||
box.appendChild(msg);
|
||||
|
||||
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(false); // Pass false for Cancel
|
||||
};
|
||||
btndiv.appendChild(btnCancel);
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Modal für Passwort-Eingabe (z.B. Passwort-Reset aus der User-Liste)
|
||||
function showInputModal(message, callback) {
|
||||
document.querySelectorAll('.custom-modal-popup').forEach(m => m.remove());
|
||||
|
||||
let modal = document.createElement('div');
|
||||
modal.className = 'custom-modal-popup';
|
||||
|
||||
let box = document.createElement('div');
|
||||
box.className = 'custom-modal-box';
|
||||
|
||||
let msg = document.createElement('div');
|
||||
msg.className = 'custom-modal-msg';
|
||||
msg.textContent = message;
|
||||
box.appendChild(msg);
|
||||
|
||||
let input = document.createElement('input');
|
||||
input.type = 'password';
|
||||
input.className = 'custom-modal-input';
|
||||
box.appendChild(input);
|
||||
|
||||
let btndiv = document.createElement('div');
|
||||
btndiv.className = 'twobuttons';
|
||||
|
||||
const close = () => {
|
||||
if (modal.parentNode) modal.parentNode.removeChild(modal);
|
||||
};
|
||||
|
||||
let btnCancel = document.createElement('button');
|
||||
btnCancel.className = 'custom-modal-btn';
|
||||
btnCancel.textContent = 'Abbruch';
|
||||
btnCancel.onclick = () => {
|
||||
close();
|
||||
callback(null);
|
||||
};
|
||||
btndiv.appendChild(btnCancel);
|
||||
|
||||
let btnOk = document.createElement('button');
|
||||
btnOk.className = 'custom-modal-btn';
|
||||
btnOk.textContent = 'OK';
|
||||
btnOk.onclick = () => {
|
||||
const value = input.value;
|
||||
close();
|
||||
callback(value || null);
|
||||
};
|
||||
btndiv.appendChild(btnOk);
|
||||
|
||||
box.appendChild(btndiv);
|
||||
modal.appendChild(box);
|
||||
document.body.appendChild(modal);
|
||||
input.focus();
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
btnOk.click();
|
||||
}
|
||||
});
|
||||
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) {
|
||||
close();
|
||||
callback(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 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 (Object.hasOwn(data.props, "chip")) {
|
||||
let pp = data.props.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', false, () => {
|
||||
sensorNumberInput.disabled = false;
|
||||
sensorNumberInput.focus();
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Abrufen der Adresse:', err);
|
||||
addressInput.value = '';
|
||||
sensorNumberInput.disabled = true;
|
||||
showModal('Sensor unbekannt', false, () => {
|
||||
sensorNumberInput.disabled = false;
|
||||
sensorNumberInput.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enter-Taste
|
||||
sensorNumberInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
fetchAddressIfValid();
|
||||
}
|
||||
});
|
||||
|
||||
// Feld verlassen
|
||||
sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
|
||||
|
||||
|
||||
async function saveEntry() {
|
||||
const espId = espIdInput.value.trim();
|
||||
const sensorNumber = sensorNumberInput.value.trim();
|
||||
const name = nameInput.value.trim();
|
||||
const description = descriptionInput.value.trim();
|
||||
const address = addressInput.value.trim();
|
||||
|
||||
if (!espId || !sensorNumber) {
|
||||
resultDiv.textContent = 'ESP-ID und Sensornummer sind Pflichtfelder.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = '/api/save';
|
||||
const method = 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ espId, sensorNumber, name, description, address })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
resultDiv.textContent = data.error;
|
||||
} else {
|
||||
resultDiv.textContent = 'OK!';
|
||||
setTimeout(() => {
|
||||
resultDiv.textContent = ''
|
||||
saveBtn.textContent = 'Speichern';
|
||||
}, 5000)
|
||||
clearForm(false);
|
||||
await loadEntries();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resultDiv.textContent = 'Fehler beim Speichern.';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function clearForm(mitButton) {
|
||||
espIdInput.value = '';
|
||||
sensorNumberInput.value = '';
|
||||
nameInput.value = '';
|
||||
descriptionInput.value = '';
|
||||
addressInput.value = '';
|
||||
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) || 50;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/list?page=${page}&limit=${limit}`);
|
||||
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 = '';
|
||||
sortedItems.forEach(item => {
|
||||
const date = new Date(item.chip.lastUpdatedAt).toISOString().split('T')[0];
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<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>
|
||||
<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 () => {
|
||||
const id = btn.getAttribute('data-id');
|
||||
await deleteEntry(id);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.editBtn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
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 === parseInt(id));
|
||||
if (item) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resultDiv.textContent = 'Fehler beim Laden.';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEntry(id) {
|
||||
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.';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
const usersTableBody = document.querySelector('#usersTable tbody');
|
||||
const usersResult = document.getElementById('usersResult');
|
||||
if (!usersTableBody) return;
|
||||
try {
|
||||
const res = await fetch('/api/users');
|
||||
const users = await res.json();
|
||||
usersTableBody.innerHTML = '';
|
||||
users.forEach(u => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${u.email}</td>
|
||||
<td>${u.role}</td>
|
||||
<td>
|
||||
<div class="twobuttons">
|
||||
<button data-id="${u._id}" data-email="${u.email}" class="editBtn resetUserBtn" title="Passwort zurücksetzen">🔑</button>
|
||||
<button data-id="${u._id}" class="deleteBtn deleteUserBtn" title="Löschen">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
usersTableBody.appendChild(tr);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.resetUserBtn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const email = btn.getAttribute('data-email');
|
||||
showInputModal(`Neues Passwort für ${email}:`, async (newPassword) => {
|
||||
if (!newPassword) return;
|
||||
try {
|
||||
const res = await fetch('/api/resetPassword', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, newPassword })
|
||||
});
|
||||
const data = await res.json();
|
||||
usersResult.textContent = data.success ? `Passwort für ${email} zurückgesetzt.` : (data.error || 'Fehler beim Zurücksetzen.');
|
||||
} catch (err) {
|
||||
usersResult.textContent = 'Serverfehler.';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.deleteUserBtn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.getAttribute('data-id');
|
||||
showModal('User wirklich löschen?', true, async (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
usersResult.textContent = 'User gelöscht.';
|
||||
await loadUsers();
|
||||
} else {
|
||||
usersResult.textContent = data.error || 'Fehler beim Löschen.';
|
||||
}
|
||||
} catch (err) {
|
||||
usersResult.textContent = 'Serverfehler.';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
saveBtn.addEventListener('click', saveEntry);
|
||||
refreshBtn.addEventListener('click', loadEntries);
|
||||
cancelBtn.addEventListener('click', () => clearForm(true));
|
||||
if (userCancelBtn) 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');
|
||||
clearUserForm();
|
||||
const userResult = document.getElementById('userResult');
|
||||
if (userResult) userResult.textContent = '';
|
||||
const usersResult = document.getElementById('usersResult');
|
||||
if (usersResult) usersResult.textContent = '';
|
||||
loadUsers();
|
||||
})
|
||||
|
||||
loadEntries();
|
||||
window.loadUsers = loadUsers;
|
||||
});
|
||||
|
||||
window.showTab = showTab;
|
||||
@@ -0,0 +1,34 @@
|
||||
// public/login.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;
|
||||
}
|
||||
|
||||
debounceTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/check-email?email=${encodeURIComponent(email)}`);
|
||||
const data = await res.json();
|
||||
if (data.exists) {
|
||||
emailStatus.textContent = '✅ Benutzer existiert';
|
||||
emailStatus.style.color = 'green';
|
||||
} else {
|
||||
emailStatus.textContent = '❌ Benutzer nicht gefunden';
|
||||
emailStatus.style.color = 'red';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
emailStatus.textContent = 'Fehler bei der Prüfung';
|
||||
emailStatus.style.color = 'orange';
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Tab Navigation */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.tab-btn {
|
||||
background: #eee;
|
||||
border: none;
|
||||
padding: 0.7rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
border-radius: 6px 6px 0 0;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
.tab-btn.active {
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
|
||||
}
|
||||
|
||||
#tabUser {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
#logoutBtn {
|
||||
margin-left: auto;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Modal Fehlerfenster */
|
||||
.custom-modal-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0,0,0,0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.custom-modal-box {
|
||||
background: #fff;
|
||||
padding: 3rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
|
||||
text-align: center;
|
||||
min-width: 350px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.custom-modal-msg {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.5rem;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.custom-modal-input {
|
||||
display: block;
|
||||
width: 80%;
|
||||
margin: 0 auto 1.5rem auto;
|
||||
}
|
||||
|
||||
.custom-modal-btn {
|
||||
padding: 0.8rem 2.5rem;
|
||||
font-size: 1.1rem;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
input, button {
|
||||
font-size: 1rem;
|
||||
padding: 8px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
background-color: antiquewhite;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.controls input {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
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;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
background-color:burlywood
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.6rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.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;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start; /* Links bündig */
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card form label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card form input,
|
||||
.card form textarea {
|
||||
width: 100%;
|
||||
max-width: 400px; /* gleiche Breite */
|
||||
padding: 0.4rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card form textarea {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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 APIHOST = process.env.APIHOST || 'https://noise.fuerst-stuttgart.de/srv/';
|
||||
|
||||
|
||||
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) {
|
||||
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();
|
||||
addressString = [street, rightPart].filter(Boolean).join(', ');
|
||||
addrParts = { street, plz, city };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Address lookup failed:', err);
|
||||
}
|
||||
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 { propCollection, prop_fluxCollection } = getCollections();
|
||||
|
||||
// Suche nach Sensornummer als _id
|
||||
const propEntry = await propCollection.findOne({ _id: sensorNumber });
|
||||
if (!propEntry) {
|
||||
await clientClose()
|
||||
return res.status(404).json({ error: 'Sensor nicht gefunden' });
|
||||
}
|
||||
const adr = await holAdresse(encodeURIComponent(propEntry._id))
|
||||
adr.props = propEntry
|
||||
|
||||
return res.json(adr)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { MongoAPIError, ObjectId } from 'mongodb';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { getCollections, update_pflux } from '../db/mongo.js';
|
||||
|
||||
export function registerApiRoutes(app, requireLogin) {
|
||||
const { usersCollection, propCollection } = 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:`${email}` });
|
||||
res.json({ exists: !!existingUser });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Fehler bei der E-Mail-Prüfung' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/save', requireLogin, async (req, res) => {
|
||||
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 = {
|
||||
id: espId,
|
||||
name: name || '',
|
||||
description: description || '',
|
||||
lastUpdatedAt: new Date()
|
||||
};
|
||||
await update_pflux(sensorNumber, doc)
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Fehler beim Speichern' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.get('/api/list', requireLogin, async (req, res) => {
|
||||
const { id } = req.query;
|
||||
if (id) {
|
||||
try {
|
||||
const item = await propCollection.findOne({ _id: parseInt(id) });
|
||||
if (item) return res.json([item]);
|
||||
return res.json([]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
||||
let gesamtZahl = 0
|
||||
try {
|
||||
gesamtZahl = await propCollection.countDocuments({chip: {$exists: true}})
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const skip = (page - 1) * limit;
|
||||
try {
|
||||
const items = await propCollection.find({chip: {$exists: true}})
|
||||
.sort({ "chip.lastUpdatedAt": -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
const data = {items: items, anzahl: gesamtZahl}
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Fehler beim Laden' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/delete/:id', requireLogin, async (req, res) => {
|
||||
await propCollection.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' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/changePassword', requireLogin, async (req, res) => {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
if (!currentPassword || !newPassword) {
|
||||
return res.status(400).json({ error: 'Aktuelles und neues Passwort erforderlich' });
|
||||
}
|
||||
try {
|
||||
const user = await usersCollection.findOne({ _id: new ObjectId(req.session.userId) });
|
||||
if (!user) return res.status(404).json({ error: 'User nicht gefunden' });
|
||||
const match = await bcrypt.compare(currentPassword, user.passwordHash);
|
||||
if (!match) return res.status(403).json({ error: 'Aktuelles Passwort ist falsch' });
|
||||
const hash = await bcrypt.hash(newPassword, 10);
|
||||
await usersCollection.updateOne({ _id: user._id }, { $set: { passwordHash: hash } });
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Fehler beim Ändern des Passworts' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/resetPassword', requireLogin, async (req, res) => {
|
||||
if (!req.session.isAdmin) return res.status(403).json({ error: 'Nur Admins erlaubt' });
|
||||
const { email, newPassword } = req.body;
|
||||
if (!email || !newPassword) return res.status(400).json({ error: 'Email und neues Passwort erforderlich' });
|
||||
try {
|
||||
const hash = await bcrypt.hash(newPassword, 10);
|
||||
const result = await usersCollection.updateOne(
|
||||
{ email: email.toLowerCase() },
|
||||
{ $set: { passwordHash: hash } }
|
||||
);
|
||||
if (result.matchedCount === 0) return res.status(404).json({ error: 'User nicht gefunden' });
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Fehler beim Zurücksetzen' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/users', requireLogin, async (req, res) => {
|
||||
if (!req.session.isAdmin) return res.status(403).json({ error: 'Nur Admins erlaubt' });
|
||||
try {
|
||||
const users = await usersCollection.find({}, { projection: { email: 1, role: 1 } }).sort({ email: 1 }).toArray();
|
||||
res.json(users);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Fehler beim Laden der User' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/users/:id', requireLogin, async (req, res) => {
|
||||
if (!req.session.isAdmin) return res.status(403).json({ error: 'Nur Admins erlaubt' });
|
||||
const { id } = req.params;
|
||||
if (id === String(req.session.userId)) {
|
||||
return res.status(400).json({ error: 'Der eigene Account kann nicht gelöscht werden' });
|
||||
}
|
||||
try {
|
||||
const result = await usersCollection.deleteOne({ _id: new ObjectId(id) });
|
||||
if (result.deletedCount === 0) return res.status(404).json({ error: 'User nicht gefunden' });
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Fehler beim Löschen' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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('/login', async (req, res) => {
|
||||
const version = pkg.version
|
||||
const vdate = pkg.date
|
||||
const noUsers = (await usersCollection.countDocuments()) === 0;
|
||||
res.render('login', {
|
||||
error: null,
|
||||
version: version,
|
||||
vdate: vdate,
|
||||
noUsers: noUsers
|
||||
})
|
||||
});
|
||||
|
||||
app.post('/login', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
const version = pkg.version
|
||||
const vdate = pkg.date
|
||||
const noUsers = (await usersCollection.countDocuments()) === 0;
|
||||
const renderError = () => res.render('login', { error: errText, version, vdate, noUsers });
|
||||
const user = await usersCollection.findOne({ email: email.toLowerCase() });
|
||||
if (!user) return renderError();
|
||||
const match = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!match) return renderError();
|
||||
req.session.userId = user._id;
|
||||
req.session.isAdmin = user.role === 'admin';
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
app.get('/logout', (req, res) => {
|
||||
req.session.destroy(() => res.redirect('/login'));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import express from 'express';
|
||||
import session from 'express-session';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
import bcrypt from 'bcrypt';
|
||||
import pkg from './package.json' with { type: "json" }
|
||||
dotenv.config();
|
||||
|
||||
import { initMongo, getCollections } from './db/mongo.js';
|
||||
import { registerApiRoutes } from './routes/api.js';
|
||||
import { registerAuthRoutes } from './routes/auth.js';
|
||||
import { registerAddressRoute } from './routes/address.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const SESSION_SECRET = process.env.SESSION_SECRET || 'supersecret';
|
||||
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use(session({
|
||||
secret: SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: { maxAge: 24 * 60 * 60 * 1000 }
|
||||
}));
|
||||
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'pug');
|
||||
|
||||
// DB verbinden
|
||||
await initMongo();
|
||||
await bootstrapAdmin();
|
||||
|
||||
// Legt beim allerersten Start (leere user-Collection) einen Admin aus
|
||||
// ADMIN_EMAIL/ADMIN_PASSWORD an - notwendig für extern verwaltete Deployments
|
||||
// ohne direkten Mongo-Zugriff (z.B. per Portainer betriebene Server).
|
||||
async function bootstrapAdmin() {
|
||||
const { usersCollection } = getCollections();
|
||||
const count = await usersCollection.countDocuments();
|
||||
if (count > 0) return;
|
||||
|
||||
const email = process.env.ADMIN_EMAIL;
|
||||
const password = process.env.ADMIN_PASSWORD;
|
||||
if (!email || !password) {
|
||||
console.warn('Keine User in der Datenbank und ADMIN_EMAIL/ADMIN_PASSWORD nicht gesetzt — es kann sich niemand einloggen.');
|
||||
return;
|
||||
}
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
await usersCollection.insertOne({ email: email.toLowerCase(), passwordHash, role: 'admin' });
|
||||
console.log(`Bootstrap-Admin angelegt: ${email.toLowerCase()}`);
|
||||
}
|
||||
|
||||
// Login-Middleware
|
||||
function requireLogin(req, res, next) {
|
||||
// Entwicklungs-Bypass: Automatischer Login als Admin
|
||||
if (process.env.DEV_AUTO_LOGIN === 'true') {
|
||||
req.session.userId = 'dev-user';
|
||||
req.session.isAdmin = true;
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.session.userId) {
|
||||
return next();
|
||||
}
|
||||
res.redirect('/login');
|
||||
}
|
||||
|
||||
// Routen registrieren
|
||||
registerAuthRoutes(app);
|
||||
registerApiRoutes(app, requireLogin);
|
||||
registerAddressRoute(app, requireLogin);
|
||||
|
||||
// Hauptseite
|
||||
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}`));
|
||||
@@ -0,0 +1,166 @@
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const request = require('supertest');
|
||||
|
||||
// ...existing code...
|
||||
// tests/server.test.js
|
||||
describe('Server.js API', () => {
|
||||
let app;
|
||||
let entries = [];
|
||||
let users = [];
|
||||
|
||||
beforeEach(() => {
|
||||
app = express();
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
// Mock session middleware
|
||||
app.use((req, res, next) => { req.session = {}; next(); });
|
||||
|
||||
// /api/check-email
|
||||
app.get('/api/check-email', (req, res) => {
|
||||
const email = (req.query.email || '').toLowerCase().trim();
|
||||
if (!email) return res.json({ exists: false });
|
||||
const existingUser = users.find(u => u.email === email);
|
||||
res.json({ exists: !!existingUser });
|
||||
});
|
||||
|
||||
// /api/save
|
||||
app.post('/api/save', (req, res) => {
|
||||
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);
|
||||
const doc = { espId, sensorNumber, name, description, address, lastUpdatedAt: new Date(), _id: String(entries.length + 1) };
|
||||
entries.push(doc);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// /api/update/:id
|
||||
app.put('/api/update/:id', (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);
|
||||
const idx = entries.findIndex(e => e._id === id);
|
||||
if (idx === -1) return res.status(404).json({ error: 'Not found' });
|
||||
entries[idx] = { ...entries[idx], espId, sensorNumber, name, description, address };
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// /api/list
|
||||
app.get('/api/list', (req, res) => {
|
||||
const { id } = req.query;
|
||||
if (id) {
|
||||
const item = entries.find(e => e._id === id);
|
||||
return res.json(item ? [item] : []);
|
||||
}
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
res.json(entries.slice(skip, skip + limit));
|
||||
});
|
||||
|
||||
// /api/delete/:id
|
||||
app.delete('/api/delete/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
entries = entries.filter(e => e._id !== id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// /api/address/:sensorNumber
|
||||
app.get('/api/address/:sensorNumber', (req, res) => {
|
||||
const sensorNumber = parseInt(req.params.sensorNumber, 10);
|
||||
if (isNaN(sensorNumber)) {
|
||||
return res.status(400).json({ error: 'Ungültige Sensornummer' });
|
||||
}
|
||||
// Dummy logic
|
||||
if (sensorNumber === 1001) {
|
||||
return res.json({ address: 'Musterstraße 1, 12345 Musterstadt', parts: { street: 'Musterstraße 1', plz: '12345', city: 'Musterstadt' } });
|
||||
}
|
||||
return res.status(404).json({ error: 'Sensor unbekannt' });
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
entries = [];
|
||||
users = [{ email: 'test@example.com', passwordHash: 'hash' }];
|
||||
});
|
||||
|
||||
test('GET /api/check-email returns exists: true for known user', async () => {
|
||||
const res = await request(app).get('/api/check-email?email=test@example.com');
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toHaveProperty('exists', true);
|
||||
});
|
||||
|
||||
test('GET /api/check-email returns exists: false for unknown user', async () => {
|
||||
const res = await request(app).get('/api/check-email?email=unknown@example.com');
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toHaveProperty('exists', false);
|
||||
});
|
||||
|
||||
test('POST /api/save creates entry', async () => {
|
||||
const res = await request(app).post('/api/save').send({ espId: 'esp1', sensorNumber: '1001', name: 'Test', description: 'Desc', address: 'Addr' });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toHaveProperty('success', true);
|
||||
expect(entries.length).toBe(1);
|
||||
});
|
||||
|
||||
test('POST /api/save fails without espId', async () => {
|
||||
const res = await request(app).post('/api/save').send({ sensorNumber: '1001' });
|
||||
expect(res.body).toHaveProperty('error');
|
||||
});
|
||||
|
||||
test('PUT /api/update/:id updates entry', async () => {
|
||||
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');
|
||||
});
|
||||
|
||||
test('PUT /api/update/:id fails for unknown id', async () => {
|
||||
const res = await request(app).put('/api/update/999').send({ espId: 'esp2', sensorNumber: '1002', name: 'Neu', description: 'Neu', address: 'Neu' });
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('GET /api/list returns all entries', async () => {
|
||||
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: '', 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: '', lastUpdatedAt: new Date() });
|
||||
const res = await request(app).delete('/api/delete/1');
|
||||
expect(res.body).toHaveProperty('success', true);
|
||||
expect(entries.length).toBe(0);
|
||||
});
|
||||
|
||||
test('GET /api/address/:sensorNumber returns address for known sensor', async () => {
|
||||
const res = await request(app).get('/api/address/1001');
|
||||
expect(res.body).toHaveProperty('address', 'Musterstraße 1, 12345 Musterstadt');
|
||||
});
|
||||
|
||||
test('GET /api/address/:sensorNumber returns error for unknown sensor', async () => {
|
||||
const res = await request(app).get('/api/address/9999');
|
||||
expect(res.statusCode).toBe(404);
|
||||
expect(res.body).toHaveProperty('error', 'Sensor unbekannt');
|
||||
});
|
||||
|
||||
test('GET /api/address/:sensorNumber returns error for invalid sensor', async () => {
|
||||
const res = await request(app).get('/api/address/abc');
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(res.body).toHaveProperty('error', 'Ungültige Sensornummer');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
doctype html
|
||||
html(lang="de")
|
||||
head
|
||||
meta(charset="utf-8")
|
||||
meta(name="viewport", content="width=device-width, initial-scale=1")
|
||||
title ESP-ID zu Sensornummer
|
||||
link(rel="stylesheet", href="/styles.css")
|
||||
body
|
||||
h1 ESP-ID → Sensornummer
|
||||
// Tab Navigation
|
||||
div.tabs
|
||||
button.tab-btn#tabInput.active(type="button" onclick="showTab('input')") Eingabe
|
||||
button.tab-btn#tabList(type="button" onclick="showTab('list')") Liste
|
||||
button.tab-btn#tabProfile(type="button" onclick="showTab('profile')") Profil
|
||||
if isAdmin
|
||||
button.tab-btn#tabUser(type="button" onclick="showTab('user')") User
|
||||
a.tab-btn#logoutBtn(href="/logout") Logout
|
||||
|
||||
// Eingabe-Tab
|
||||
div#tabInputContent.tab-content
|
||||
div.card
|
||||
form#entryForm
|
||||
label(for="sensorNumber") Sensornummer:
|
||||
input#sensorNumber(type="text" placeholder="Nur Zahlen erlaubt")
|
||||
|
||||
label(for="espId") ESP-ID:
|
||||
input#espId(type="text")
|
||||
|
||||
label(for="name") Bezeichnung:
|
||||
input#name(type="text")
|
||||
|
||||
label(for="description") Beschreibung:
|
||||
textarea#description
|
||||
|
||||
label(for="address") Anschrift:
|
||||
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")
|
||||
div.controls
|
||||
button#refreshBtn Aktualisieren
|
||||
| Seite:
|
||||
input#page(value="1")
|
||||
| Limit:
|
||||
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(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(id="thDate" data-sort="date" style="cursor:pointer") Datum <span id="sortArrowDate">↑</span>
|
||||
th Aktionen
|
||||
tbody
|
||||
|
||||
// Profil-Tab (für alle eingeloggten User)
|
||||
div#tabProfileContent.tab-content(style="display:none")
|
||||
div.card
|
||||
h2 Passwort ändern
|
||||
form#profileForm
|
||||
label(for="currentPassword") Aktuelles Passwort:
|
||||
input#currentPassword(type="password" required)
|
||||
label(for="newPassword") Neues Passwort:
|
||||
input#newPassword(type="password" required)
|
||||
label(for="newPasswordRepeat") Neues Passwort (Wiederholung):
|
||||
input#newPasswordRepeat(type="password" required)
|
||||
.twobuttons
|
||||
button#profileSaveBtn(type="button") Ändern
|
||||
div#profileResult
|
||||
#version Version: #{version} vom #{vdate}
|
||||
|
||||
// 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
|
||||
div.card
|
||||
h2 Angemeldete User
|
||||
table#usersTable
|
||||
thead
|
||||
tr
|
||||
th Email
|
||||
th Rolle
|
||||
th Aktionen
|
||||
tbody
|
||||
div#usersResult
|
||||
#version Version: #{version} vom #{vdate}
|
||||
|
||||
script(type="module" src="/global.js")
|
||||
@@ -0,0 +1,27 @@
|
||||
doctype html
|
||||
html(lang="de")
|
||||
head
|
||||
meta(charset="utf-8")
|
||||
meta(name="viewport", content="width=device-width, initial-scale=1")
|
||||
title Login
|
||||
link(rel="stylesheet", href="/styles.css")
|
||||
body
|
||||
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)
|
||||
span#emailStatus
|
||||
label(for="password") Passwort:
|
||||
input#password(type="password" name="password" required)
|
||||
button(type="submit") Login
|
||||
p.hint Passwort vergessen? Bitte an einen Admin wenden.
|
||||
if noUsers
|
||||
p.hint
|
||||
strong Es ist noch kein User angelegt. Bitte ADMIN_EMAIL und ADMIN_PASSWORD als Umgebungsvariablen setzen und den Container neu starten.
|
||||
#version Version: #{version} vom #{vdate}
|
||||
|
||||
if error
|
||||
p.error= error
|
||||
script(type="module" src="/login.js")
|
||||
+3
-25
@@ -6,11 +6,9 @@ set -e
|
||||
# Nur linux/amd64, nativer Build auf esprimo (amd64) -> kein buildx/qemu,
|
||||
# eigener Dockerfile-Name (Dockerfile_noise). Image-Name: noise.
|
||||
|
||||
REGISTRY="docker.citysensor.de"
|
||||
# Zweite Registry (Gitea). Das Image wird dorthin zusaetzlich gepusht, damit es
|
||||
# nicht nur an einer Stelle liegt. Leersetzen schaltet den zweiten Push ab:
|
||||
# REGISTRY2= ./deploy.sh
|
||||
REGISTRY2="${REGISTRY2-gitea.fuerst-stuttgart.de/admin}"
|
||||
# Ziel-Registry, per Umgebungsvariable ueberschreibbar:
|
||||
# REGISTRY=anderes.registry.example ./deploy.sh
|
||||
REGISTRY="${REGISTRY:-ghcr.io/dnms-org/laermkarte-sensor-community}"
|
||||
IMAGE_NAME="noise"
|
||||
TAG="${1:-latest}"
|
||||
PLATFORM="linux/amd64"
|
||||
@@ -54,26 +52,6 @@ if [ "${TAG}" != "latest" ]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Dasselbe Image zusaetzlich in die zweite Registry. Ein Fehlschlag bricht das
|
||||
# Skript nicht ab: in ${REGISTRY} liegt das Image dann bereits, der Stack kann
|
||||
# es also ziehen - die Meldung sagt nur, dass die Zweitablage fehlt.
|
||||
if [ -n "${REGISTRY2}" ]; then
|
||||
echo ">>> Pushe zusaetzlich nach ${REGISTRY2}/${IMAGE_NAME} ..."
|
||||
ok=true
|
||||
docker tag "${FULL_IMAGE}" "${REGISTRY2}/${IMAGE_NAME}:${TAG}"
|
||||
docker push "${REGISTRY2}/${IMAGE_NAME}:${TAG}" || ok=false
|
||||
if [ "${ok}" = true ] && [ "${TAG}" != "latest" ]; then
|
||||
docker tag "${FULL_IMAGE}" "${REGISTRY2}/${IMAGE_NAME}:latest"
|
||||
docker push "${REGISTRY2}/${IMAGE_NAME}:latest" || ok=false
|
||||
fi
|
||||
if [ "${ok}" != true ]; then
|
||||
echo "!!! Push nach ${REGISTRY2} fehlgeschlagen."
|
||||
echo "!!! Angemeldet? docker login ${REGISTRY2%%/*}"
|
||||
echo "!!! ${FULL_IMAGE} ist gepusht und einsetzbar."
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Build + Push fertig: ${FULL_IMAGE}"
|
||||
echo "=========================================="
|
||||
|
||||
@@ -26,7 +26,6 @@ html
|
||||
footer
|
||||
#author
|
||||
#mailadr
|
||||
a(href="mailto:rexfue@gmail.com") mailto:rexfue@gmail.com
|
||||
#versn #{t('Version')}: #{version} #{t('from')} #{date}
|
||||
|
||||
script(src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
|
||||
|
||||
@@ -12,6 +12,9 @@ LOCALDIR=<Aktuelle Direcrory ab />
|
||||
# Datenbank, in welche die laufenden Messwerte gespeichert werden (kannn 'influx' oder 'mongo' sein)
|
||||
STORE=influx
|
||||
|
||||
# Secret zum Signieren der Session-Cookies von esp2sensor (langer Zufallsstring)
|
||||
ESP2SENSOR_SESSION_SECRET=<zufaelliger_string>
|
||||
|
||||
# Nur fuer readarchive-stack.yml: Zeitraum und Ziel des Einmal-Laufs.
|
||||
# Leer lassen = Default (gestern, ein Tag, TYP=noise, DBASE=influx).
|
||||
# END ist exklusiv.
|
||||
|
||||
@@ -79,8 +79,24 @@ services:
|
||||
- ${LOCALDIR}/noisesensors/log:/var/log
|
||||
container_name: noise
|
||||
environment:
|
||||
APIHOST: http://sensorapi:3005
|
||||
ports:
|
||||
APIHOST: http://sensorapi:3005
|
||||
ports:
|
||||
- "3003:3000"
|
||||
restart: unless-stopped
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
esp2sensor:
|
||||
image: docker.citysensor.de/esp2sensor
|
||||
container_name: esp2sensor
|
||||
volumes:
|
||||
- ${LOCALDIR}/noisesensors/log:/var/log
|
||||
environment:
|
||||
MONGOHOST: mongodb
|
||||
MONGOAUTH: true
|
||||
MONGOUSRP: ${MONGO_ROOT_USERNAME}:${MONGO_ROOT_PASSWORD}
|
||||
SESSION_SECRET: ${ESP2SENSOR_SESSION_SECRET}
|
||||
DEV_AUTO_LOGIN: "false"
|
||||
APIHOST: http://sensorapi:3005/api/
|
||||
ports:
|
||||
- "3004:3000"
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
+3
-25
@@ -6,11 +6,9 @@ set -e
|
||||
# Unterschied: Build-Kontext ist das Repository-Wurzelverzeichnis, weil
|
||||
# readarchive und readin sich die Module unter common/ teilen.
|
||||
|
||||
REGISTRY="docker.citysensor.de"
|
||||
# Zweite Registry (Gitea). Das Image wird dorthin zusaetzlich gepusht, damit es
|
||||
# nicht nur an einer Stelle liegt. Leersetzen schaltet den zweiten Push ab:
|
||||
# REGISTRY2= ./deploy.sh
|
||||
REGISTRY2="${REGISTRY2-gitea.fuerst-stuttgart.de/admin}"
|
||||
# Ziel-Registry, per Umgebungsvariable ueberschreibbar:
|
||||
# REGISTRY=anderes.registry.example ./deploy.sh
|
||||
REGISTRY="${REGISTRY:-ghcr.io/dnms-org/laermkarte-sensor-community}"
|
||||
IMAGE_NAME="readarchive"
|
||||
TAG="${1:-latest}"
|
||||
PLATFORM="linux/amd64"
|
||||
@@ -58,26 +56,6 @@ if [ "${TAG}" != "latest" ]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Dasselbe Image zusaetzlich in die zweite Registry. Ein Fehlschlag bricht das
|
||||
# Skript nicht ab: in ${REGISTRY} liegt das Image dann bereits, der Stack kann
|
||||
# es also ziehen - die Meldung sagt nur, dass die Zweitablage fehlt.
|
||||
if [ -n "${REGISTRY2}" ]; then
|
||||
echo ">>> Pushe zusaetzlich nach ${REGISTRY2}/${IMAGE_NAME} ..."
|
||||
ok=true
|
||||
docker tag "${FULL_IMAGE}" "${REGISTRY2}/${IMAGE_NAME}:${TAG}"
|
||||
docker push "${REGISTRY2}/${IMAGE_NAME}:${TAG}" || ok=false
|
||||
if [ "${ok}" = true ] && [ "${TAG}" != "latest" ]; then
|
||||
docker tag "${FULL_IMAGE}" "${REGISTRY2}/${IMAGE_NAME}:latest"
|
||||
docker push "${REGISTRY2}/${IMAGE_NAME}:latest" || ok=false
|
||||
fi
|
||||
if [ "${ok}" != true ]; then
|
||||
echo "!!! Push nach ${REGISTRY2} fehlgeschlagen."
|
||||
echo "!!! Angemeldet? docker login ${REGISTRY2%%/*}"
|
||||
echo "!!! ${FULL_IMAGE} ist gepusht und einsetzbar."
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Build + Push fertig: ${FULL_IMAGE}"
|
||||
echo "=========================================="
|
||||
|
||||
+3
-25
@@ -8,11 +8,9 @@ set -e
|
||||
# Build-Kontext ist das Repository-Wurzelverzeichnis, weil readin und
|
||||
# readarchive sich die Module unter common/ teilen.
|
||||
|
||||
REGISTRY="docker.citysensor.de"
|
||||
# Zweite Registry (Gitea). Das Image wird dorthin zusaetzlich gepusht, damit es
|
||||
# nicht nur an einer Stelle liegt. Leersetzen schaltet den zweiten Push ab:
|
||||
# REGISTRY2= ./deploy.sh
|
||||
REGISTRY2="${REGISTRY2-gitea.fuerst-stuttgart.de/admin}"
|
||||
# Ziel-Registry, per Umgebungsvariable ueberschreibbar:
|
||||
# REGISTRY=anderes.registry.example ./deploy.sh
|
||||
REGISTRY="${REGISTRY:-ghcr.io/dnms-org/laermkarte-sensor-community}"
|
||||
IMAGE_NAME="readin"
|
||||
TAG="${1:-latest}"
|
||||
PLATFORM="linux/amd64"
|
||||
@@ -60,26 +58,6 @@ if [ "${TAG}" != "latest" ]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Dasselbe Image zusaetzlich in die zweite Registry. Ein Fehlschlag bricht das
|
||||
# Skript nicht ab: in ${REGISTRY} liegt das Image dann bereits, der Stack kann
|
||||
# es also ziehen - die Meldung sagt nur, dass die Zweitablage fehlt.
|
||||
if [ -n "${REGISTRY2}" ]; then
|
||||
echo ">>> Pushe zusaetzlich nach ${REGISTRY2}/${IMAGE_NAME} ..."
|
||||
ok=true
|
||||
docker tag "${FULL_IMAGE}" "${REGISTRY2}/${IMAGE_NAME}:${TAG}"
|
||||
docker push "${REGISTRY2}/${IMAGE_NAME}:${TAG}" || ok=false
|
||||
if [ "${ok}" = true ] && [ "${TAG}" != "latest" ]; then
|
||||
docker tag "${FULL_IMAGE}" "${REGISTRY2}/${IMAGE_NAME}:latest"
|
||||
docker push "${REGISTRY2}/${IMAGE_NAME}:latest" || ok=false
|
||||
fi
|
||||
if [ "${ok}" != true ]; then
|
||||
echo "!!! Push nach ${REGISTRY2} fehlgeschlagen."
|
||||
echo "!!! Angemeldet? docker login ${REGISTRY2%%/*}"
|
||||
echo "!!! ${FULL_IMAGE} ist gepusht und einsetzbar."
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Build + Push fertig: ${FULL_IMAGE}"
|
||||
echo "=========================================="
|
||||
|
||||
@@ -8,7 +8,7 @@ const NOMINATIM_URL = `https://nominatim.openstreetmap.org/reverse?lat=${'xx'}&l
|
||||
const NOMINATIM_CITY_URL = `https://nominatim.openstreetmap.org/?q="${'xx'}"&format=json`
|
||||
// Nominatim verlangt laut Nutzungsbedingungen einen eigenen User-Agent und
|
||||
// beantwortet den Default von axios ('axios/1.x') mit 403.
|
||||
const NOMINATIM_UA = 'laermsensor-stack (rxf@gmx.de)'
|
||||
const NOMINATIM_UA = 'laermsensor-stack'
|
||||
|
||||
export const getCityCoords = async (params) => {
|
||||
let ret = {coords: [], city: params.city, err: null}
|
||||
|
||||
+3
-25
@@ -6,11 +6,9 @@ set -e
|
||||
# Nur linux/amd64, nativer Build auf esprimo (amd64) -> kein buildx/qemu,
|
||||
# eigener Dockerfile-Name (Dockerfile_sensorapi).
|
||||
|
||||
REGISTRY="docker.citysensor.de"
|
||||
# Zweite Registry (Gitea). Das Image wird dorthin zusaetzlich gepusht, damit es
|
||||
# nicht nur an einer Stelle liegt. Leersetzen schaltet den zweiten Push ab:
|
||||
# REGISTRY2= ./deploy.sh
|
||||
REGISTRY2="${REGISTRY2-gitea.fuerst-stuttgart.de/admin}"
|
||||
# Ziel-Registry, per Umgebungsvariable ueberschreibbar:
|
||||
# REGISTRY=anderes.registry.example ./deploy.sh
|
||||
REGISTRY="${REGISTRY:-ghcr.io/dnms-org/laermkarte-sensor-community}"
|
||||
IMAGE_NAME="sensorapi"
|
||||
TAG="${1:-latest}"
|
||||
PLATFORM="linux/amd64"
|
||||
@@ -54,26 +52,6 @@ if [ "${TAG}" != "latest" ]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Dasselbe Image zusaetzlich in die zweite Registry. Ein Fehlschlag bricht das
|
||||
# Skript nicht ab: in ${REGISTRY} liegt das Image dann bereits, der Stack kann
|
||||
# es also ziehen - die Meldung sagt nur, dass die Zweitablage fehlt.
|
||||
if [ -n "${REGISTRY2}" ]; then
|
||||
echo ">>> Pushe zusaetzlich nach ${REGISTRY2}/${IMAGE_NAME} ..."
|
||||
ok=true
|
||||
docker tag "${FULL_IMAGE}" "${REGISTRY2}/${IMAGE_NAME}:${TAG}"
|
||||
docker push "${REGISTRY2}/${IMAGE_NAME}:${TAG}" || ok=false
|
||||
if [ "${ok}" = true ] && [ "${TAG}" != "latest" ]; then
|
||||
docker tag "${FULL_IMAGE}" "${REGISTRY2}/${IMAGE_NAME}:latest"
|
||||
docker push "${REGISTRY2}/${IMAGE_NAME}:latest" || ok=false
|
||||
fi
|
||||
if [ "${ok}" != true ]; then
|
||||
echo "!!! Push nach ${REGISTRY2} fehlgeschlagen."
|
||||
echo "!!! Angemeldet? docker login ${REGISTRY2%%/*}"
|
||||
echo "!!! ${FULL_IMAGE} ist gepusht und einsetzbar."
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Build + Push fertig: ${FULL_IMAGE}"
|
||||
echo "=========================================="
|
||||
|
||||
Reference in New Issue
Block a user