Aufteilung in 2 Tabs

Zugriff auf ionos-Mongo
This commit is contained in:
2025-08-14 18:16:37 +00:00
parent 1da23e24c7
commit 4cffdba7df
7 changed files with 4809 additions and 74 deletions

4469
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,8 @@
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon --watch server.js --watch views --watch public server.js"
"dev": "nodemon --watch server.js --watch views --watch public server.js",
"test": "jest"
},
"dependencies": {
"bcrypt": "^6.0.0",
@@ -18,6 +19,8 @@
"pug": "^3.0.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
"jest": "^30.0.5",
"nodemon": "^3.0.1",
"supertest": "^7.1.4"
}
}

View File

@@ -13,6 +13,36 @@ document.addEventListener('DOMContentLoaded', () => {
let editId = null;
// Modal für Fehleranzeige
function showModal(message, callback) {
// Vorherige Modals entfernen
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 btn = document.createElement('button');
btn.className = 'custom-modal-btn';
btn.textContent = 'OK';
btn.onclick = () => {
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
if (callback) callback();
};
box.appendChild(btn);
modal.appendChild(box);
document.body.appendChild(modal);
}
// Sensornummer nur Zahlen erlauben
sensorNumberInput.addEventListener('input', () => {
sensorNumberInput.value = sensorNumberInput.value.replace(/\D/g, '');
@@ -29,9 +59,20 @@ async function fetchAddressIfValid() {
addressInput.value = data.address;
} else {
addressInput.value = '';
sensorNumberInput.disabled = true;
showModal('Sensor unbekannt', () => {
sensorNumberInput.disabled = false;
sensorNumberInput.focus();
});
}
} catch (err) {
console.error('Fehler beim Abrufen der Adresse:', err);
addressInput.value = '';
sensorNumberInput.disabled = true;
showModal('Sensor unbekannt', () => {
sensorNumberInput.disabled = false;
sensorNumberInput.focus();
});
}
}
}

View File

@@ -1,3 +1,65 @@
/* 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);
}
/* 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-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;
@@ -89,6 +151,10 @@ button:hover {
background: #0056b3;
}
#saveBtn {
margin-top: 20px;
}
p.error {
color: red;
font-weight: bold;

View File

@@ -4,13 +4,28 @@ import bcrypt from 'bcrypt';
import { MongoClient, ObjectId } from 'mongodb';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3000;
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
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';
// If credentials are set, inject them into the URI
if (MONGO_ROOT_USER && MONGO_ROOT_PASSWORD) {
// Remove protocol and host from URI
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 DB_NAME = process.env.DB_NAME || 'espdb';
const SESSION_SECRET = process.env.SESSION_SECRET || 'supersecret';
@@ -44,8 +59,9 @@ 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');
return next()
}
// Auth-Routen
@@ -197,36 +213,6 @@ function getAddress(sensorNumber) {
const ADDRESS_SERVICE_URL = process.env.ADDRESS_SERVICE_URL
|| 'https://noise.fuerst-stuttgart.de/srv/getaddress';
// kleine Fetch-Helferfunktion mit Timeout
async function fetchWithTimeout(url, ms = 5000) {
const ctrl = new AbortController();
const id = setTimeout(() => ctrl.abort(), ms);
try {
const res = await fetch(url, { signal: ctrl.signal, headers: { 'Accept': 'application/json, text/plain;q=0.9, */*;q=0.8' } });
return res;
} finally {
clearTimeout(id);
}
}
const normalizeAddressPayload = (contentType, payload) => {
try {
if (contentType.includes('application/json')) {
const data = payload; // schon geparst
if (typeof data === 'string') return data.trim();
if (data?.address) return String(data.address).trim();
if (data?.addr) return String(data.addr).trim();
if (data?.result?.address) return String(data.result.address).trim();
// Fallback: JSON zu String
return JSON.stringify(data);
} else {
// Text-Antwort
return String(payload).trim();
}
} catch {
return '';
}
};
// /api/address/:sensorNumber holt Adresse als String "Straße, PLZ Stadt"
app.get('/api/address/:sensorNumber', requireLogin, async (req, res) => {
@@ -235,7 +221,7 @@ app.get('/api/address/:sensorNumber', requireLogin, async (req, res) => {
return res.status(400).json({ error: 'Ungültige Sensornummer' });
}
const url = `https://noise.fuerst-stuttgart.de/srv/getaddress?sensorid=${encodeURIComponent(sensorNumber)}`;
const url = ADDRESS_SERVICE_URL + `?sensorid=${encodeURIComponent(sensorNumber)}`;
try {
const r = await fetch(url, { headers: { 'Accept': 'application/json' } });

166
tests/server.test.js Normal file
View File

@@ -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, createdAt: 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: '', createdAt: 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: '', createdAt: 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() });
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() });
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');
});
});

View File

@@ -7,42 +7,58 @@ html(lang="de")
link(rel="stylesheet", href="/styles.css")
body
h1 ESP-ID → Sensornummer
div.card
form#entryForm
label(for="espId") ESP-ID:
input#espId(type="text")
// 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
label(for="sensorNumber") Sensornummer:
input#sensorNumber(type="text" placeholder="Nur Zahlen erlaubt")
// Eingabe-Tab
div#tabInputContent.tab-content
div.card
form#entryForm
label(for="sensorNumber") Sensornummer:
input#sensorNumber(type="text" placeholder="Nur Zahlen erlaubt")
label(for="name") Bezeichnung:
input#name(type="text")
label(for="espId") ESP-ID:
input#espId(type="text")
label(for="description") Beschreibung:
textarea#description
label(for="name") Bezeichnung:
input#name(type="text")
label(for="address") Anschrift:
input#address(type="text" placeholder="Wird automatisch ausgefüllt, kann geändert werden")
label(for="description") Beschreibung:
textarea#description
button#saveBtn(type="button") Speichern
div#result
label(for="address") Anschrift:
input#address(type="text" placeholder="Wird automatisch ausgefüllt" readonly)
div.controls
button#refreshBtn Aktualisieren
| Seite:
input#page(value="1")
| Limit:
input#limit(value="10")
button#saveBtn(type="button") Speichern
div#result
table#entriesTable
thead
tr
th ESP-ID
th Sensornummer
th Bezeichnung
th Beschreibung
th Anschrift
th Datum
th Aktionen
tbody
script(type="module" src="/global.js")
// Listen-Tab
div#tabListContent.tab-content(style="display:none")
div.controls
button#refreshBtn Aktualisieren
| Seite:
input#page(value="1")
| Limit:
input#limit(value="10")
table#entriesTable
thead
tr
th SensorNr
th ESP-ID
th Bezeichnung
th Beschreibung
th Anschrift
th Datum
th Aktionen
tbody
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');
}