Aufteilung in 2 Tabs
Zugriff auf ionos-Mongo
This commit is contained in:
4469
package-lock.json
generated
4469
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
54
server.js
54
server.js
@@ -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
166
tests/server.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
Reference in New Issue
Block a user