Adresse wird aufgelöst
Optik augehübscht
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "espid2sensor",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Kleine Webapp ESP-ID <-> Sensornummer, speichern in MongoDB",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
232
public/global.js
232
public/global.js
@@ -1,76 +1,172 @@
|
||||
const resultEl = document.getElementById('result');
|
||||
const espIn = document.getElementById('espId');
|
||||
const sensorIn = document.getElementById('sensorNumber');
|
||||
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');
|
||||
|
||||
document.getElementById('saveBtn').addEventListener('click', async () => {
|
||||
const espId = espIn.value.trim();
|
||||
const sensorNumber = sensorIn.value.trim();
|
||||
if (!espId || !sensorNumber) {
|
||||
resultEl.innerText = 'Bitte ESP-ID und Sensornummer eingeben.';
|
||||
return;
|
||||
}
|
||||
resultEl.innerText = 'Speichere...';
|
||||
try {
|
||||
const r = await fetch('/api/save', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ espId, sensorNumber })
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
resultEl.innerHTML = `<strong>Gespeichert:</strong> ESP-ID = ${j.entry.espId}, Sensor = ${j.entry.sensorNumber}`;
|
||||
espIn.value = '';
|
||||
sensorIn.value = '';
|
||||
loadList();
|
||||
} else {
|
||||
resultEl.innerText = 'Fehler: ' + (j.error || 'Unbekannt');
|
||||
let editId = 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();
|
||||
if (!data.error && data.address) {
|
||||
addressInput.value = data.address;
|
||||
} else {
|
||||
addressInput.value = '';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Abrufen der Adresse:', err);
|
||||
}
|
||||
} catch {
|
||||
resultEl.innerText = 'Netzwerkfehler';
|
||||
}
|
||||
}
|
||||
|
||||
// Enter-Taste
|
||||
sensorNumberInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
fetchAddressIfValid();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadList() {
|
||||
const page = document.getElementById('page').value || 1;
|
||||
const limit = document.getElementById('limit').value || 25;
|
||||
const listEl = document.getElementById('list');
|
||||
listEl.innerText = 'Lade...';
|
||||
try {
|
||||
const r = await fetch(`/api/list?page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`);
|
||||
const j = await r.json();
|
||||
if (!j.ok) { listEl.innerText = 'Fehler beim Laden'; return; }
|
||||
if (j.items.length === 0) { listEl.innerText = 'Keine Einträge'; return;}
|
||||
let html = `<div>Ergebnis: ${j.items.length} von ${j.total} (Seite ${j.page})</div>`;
|
||||
html += '<table><thead><tr><th>Datum</th><th>ESP-ID</th><th>Sensor</th><th></th></tr></thead><tbody>';
|
||||
j.items.forEach(it => {
|
||||
html += `<tr>
|
||||
<td>${it.createdAt}</td>
|
||||
<td>${it.espId}</td>
|
||||
<td>${it.sensorNumber}</td>
|
||||
<td><button onclick="deleteEntry('${it._id}')">Löschen</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
listEl.innerHTML = html;
|
||||
} catch {
|
||||
listEl.innerText = 'Netzwerkfehler beim Laden';
|
||||
}
|
||||
}
|
||||
// Feld verlassen
|
||||
sensorNumberInput.addEventListener('blur', fetchAddressIfValid);
|
||||
|
||||
async function deleteEntry(id) {
|
||||
if (!confirm('Diesen Eintrag wirklich löschen?')) return;
|
||||
try {
|
||||
const r = await fetch(`/api/entry/${id}`, { method: 'DELETE' });
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
loadList();
|
||||
} else {
|
||||
alert('Fehler beim Löschen');
|
||||
|
||||
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;
|
||||
}
|
||||
} catch {
|
||||
alert('Netzwerkfehler');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadList);
|
||||
loadList();
|
||||
try {
|
||||
const url = editId ? `/api/update/${editId}` : '/api/save';
|
||||
const method = editId ? 'PUT' : '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 = editId ? 'Aktualisiert!' : 'Gespeichert!';
|
||||
clearForm();
|
||||
await loadEntries();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resultDiv.textContent = 'Fehler beim Speichern.';
|
||||
}
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
espIdInput.value = '';
|
||||
sensorNumberInput.value = '';
|
||||
nameInput.value = '';
|
||||
descriptionInput.value = '';
|
||||
addressInput.value = '';
|
||||
editId = null;
|
||||
saveBtn.textContent = 'Speichern';
|
||||
}
|
||||
|
||||
async function loadEntries() {
|
||||
const page = parseInt(pageInput.value) || 1;
|
||||
const limit = parseInt(limitInput.value) || 10;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/list?page=${page}&limit=${limit}`);
|
||||
const items = await res.json();
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
items.forEach(item => {
|
||||
const date = new Date(item.createdAt).toISOString().split('T')[0];
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${item.espId}</td>
|
||||
<td>${item.sensorNumber}</td>
|
||||
<td>${item.name || ''}</td>
|
||||
<td>${item.description || ''}</td>
|
||||
<td>${item.address || ''}</td>
|
||||
<td>${date}</td>
|
||||
<td>
|
||||
<button data-id="${item._id}" class="editBtn">Bearbeiten</button>
|
||||
<button data-id="${item._id}" class="deleteBtn">Löschen</button>
|
||||
</td>
|
||||
`;
|
||||
tableBody.appendChild(tr);
|
||||
});
|
||||
|
||||
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 === id);
|
||||
if (item) {
|
||||
espIdInput.value = item.espId;
|
||||
sensorNumberInput.value = item.sensorNumber;
|
||||
nameInput.value = item.name || '';
|
||||
descriptionInput.value = item.description || '';
|
||||
addressInput.value = item.address || '';
|
||||
editId = id;
|
||||
saveBtn.textContent = 'Aktualisieren';
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resultDiv.textContent = 'Fehler beim Laden.';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEntry(id) {
|
||||
if (!confirm('Wirklich löschen?')) return;
|
||||
try {
|
||||
const res = await fetch(`/api/delete/${id}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
await loadEntries();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resultDiv.textContent = 'Fehler beim Löschen.';
|
||||
}
|
||||
}
|
||||
|
||||
saveBtn.addEventListener('click', saveEntry);
|
||||
refreshBtn.addEventListener('click', loadEntries);
|
||||
|
||||
loadEntries();
|
||||
});
|
||||
34
public/login.js
Normal file
34
public/login.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
37
public/register.js
Normal file
37
public/register.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// public/register.js
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const emailInput = document.getElementById('email');
|
||||
const emailStatus = document.getElementById('emailStatus');
|
||||
|
||||
let debounceTimeout;
|
||||
|
||||
emailInput.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimeout);
|
||||
const email = emailInput.value.trim();
|
||||
|
||||
if (!email) {
|
||||
emailStatus.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 300ms warten, um zu vermeiden, dass bei jedem Tastendruck eine Anfrage rausgeht
|
||||
debounceTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/check-email?email=${encodeURIComponent(email)}`);
|
||||
const data = await res.json();
|
||||
if (data.exists) {
|
||||
emailStatus.textContent = '❌ Diese E-Mail ist schon vergeben';
|
||||
emailStatus.style.color = 'red';
|
||||
} else {
|
||||
emailStatus.textContent = '✅ E-Mail ist frei';
|
||||
emailStatus.style.color = 'green';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
emailStatus.textContent = 'Fehler bei der Prüfung';
|
||||
emailStatus.style.color = 'orange';
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
@@ -51,4 +51,71 @@ th, td {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
417
server.js
417
server.js
@@ -1,208 +1,267 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const { MongoClient, ObjectId } = require('mongodb');
|
||||
const session = require('express-session');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
const SESSION_SECRET = process.env.SESSION_SECRET || 'supersecret';
|
||||
import express from 'express';
|
||||
import session from 'express-session';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { MongoClient, ObjectId } from 'mongodb';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
|
||||
// Statische Dateien (z.B. global.js, CSS) ausliefern
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Session Middleware einrichten (vor allen Routes)
|
||||
app.use(session({
|
||||
secret: SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 1 Tag
|
||||
}));
|
||||
|
||||
// User Collection
|
||||
let usersCollection;
|
||||
|
||||
// Pug als Template Engine einrichten
|
||||
app.set('view engine', 'pug');
|
||||
app.set('views', './views');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
|
||||
const DB_NAME = process.env.DB_NAME || 'espdb';
|
||||
const SESSION_SECRET = process.env.SESSION_SECRET || 'supersecret';
|
||||
|
||||
let db, entriesCollection;
|
||||
let db, entriesCollection, usersCollection;
|
||||
|
||||
// MongoDB-Verbindung herstellen
|
||||
// Middleware
|
||||
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');
|
||||
|
||||
// MongoDB verbinden
|
||||
async function initMongo() {
|
||||
const client = new MongoClient(MONGO_URI);
|
||||
await client.connect();
|
||||
db = client.db(DB_NAME);
|
||||
entriesCollection = db.collection('entries');
|
||||
usersCollection = db.collection('users');
|
||||
// Ensure unique index on email for users collection
|
||||
await usersCollection.createIndex({ email: 1 }, { unique: true });
|
||||
console.log(`MongoDB verbunden: ${MONGO_URI}/${DB_NAME}`);
|
||||
const client = new MongoClient(MONGO_URI);
|
||||
await client.connect();
|
||||
db = client.db(DB_NAME);
|
||||
entriesCollection = db.collection('entries');
|
||||
usersCollection = db.collection('users');
|
||||
console.log(`MongoDB verbunden: ${MONGO_URI}/${DB_NAME}`);
|
||||
}
|
||||
initMongo().catch(err => {
|
||||
console.error('MongoDB Verbindungsfehler:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
await initMongo();
|
||||
|
||||
function formatDate(date) {
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// Middleware: geschützte Seiten nur wenn angemeldet
|
||||
// Login-Middleware
|
||||
function requireLogin(req, res, next) {
|
||||
if (req.session.userId) {
|
||||
next();
|
||||
} else {
|
||||
res.redirect('/login');
|
||||
}
|
||||
if (req.session.userId) return next();
|
||||
res.redirect('/login');
|
||||
}
|
||||
|
||||
// Register-Seite (Pug)
|
||||
app.get('/register', (req, res) => {
|
||||
res.render('register', { error: null });
|
||||
// Auth-Routen
|
||||
|
||||
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 });
|
||||
res.json({ exists: !!existingUser });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Fehler bei der E-Mail-Prüfung' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/register', (req, res) => res.render('register', { error: null }));
|
||||
|
||||
app.post('/register', async (req, res) => {
|
||||
try {
|
||||
let { email, password } = req.body;
|
||||
if (!email || !password) {
|
||||
return res.render('register', { error: 'Bitte Email und Passwort angeben.' });
|
||||
}
|
||||
const { email, password } = req.body;
|
||||
if (!email || !password) return res.render('register', { error: 'Bitte Email und Passwort angeben.' });
|
||||
|
||||
email = email.trim().toLowerCase();
|
||||
const existingUser = await usersCollection.findOne({ email: email.toLowerCase() });
|
||||
if (existingUser) return res.render('register', { error: 'Email schon registriert.' });
|
||||
|
||||
// Basic email format validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return res.render('register', { error: 'Ungültiges Email-Format.' });
|
||||
}
|
||||
|
||||
// Basic password strength validation (min 8 chars, at least one number and one letter)
|
||||
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
|
||||
if (!passwordRegex.test(password)) {
|
||||
return res.render('register', { error: 'Passwort muss mindestens 8 Zeichen lang sein und mindestens eine Zahl und einen Buchstaben enthalten.' });
|
||||
}
|
||||
const existingUser = await usersCollection.findOne({ email });
|
||||
if (existingUser) {
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
await usersCollection.insertOne({
|
||||
email,
|
||||
hashedPassword: hash,
|
||||
createdAt: new Date()
|
||||
});
|
||||
res.redirect('/login');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.render('register', { error: 'Serverfehler.' });
|
||||
}
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
await usersCollection.insertOne({ email: email.toLowerCase(), passwordHash: hash });
|
||||
res.redirect('/login');
|
||||
});
|
||||
|
||||
// Login-Seite (Pug)
|
||||
app.get('/login', (req, res) => {
|
||||
res.render('login', { error: null });
|
||||
});
|
||||
app.get('/login', (req, res) => res.render('login', { error: null }));
|
||||
|
||||
app.post('/login', async (req, res) => {
|
||||
let { email, password } = req.body;
|
||||
if (!email || !password) {
|
||||
return res.render('login', { error: 'Bitte Email und Passwort angeben.' });
|
||||
}
|
||||
email = email.toLowerCase();
|
||||
try {
|
||||
const user = await usersCollection.findOne({ email });
|
||||
if (!user) {
|
||||
const match = await bcrypt.compare(password, user.hashedPassword);
|
||||
if (!match) {
|
||||
return res.render('login', { error: 'Falsche Email oder Passwort.' });
|
||||
}
|
||||
req.session.userId = user._id;
|
||||
res.redirect('/');
|
||||
req.session.userId = user._id;
|
||||
res.redirect('/');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.render('login', { error: 'Serverfehler.' });
|
||||
}
|
||||
const { email, password } = req.body;
|
||||
const user = await usersCollection.findOne({ email: email.toLowerCase() });
|
||||
if (!user) return res.render('login', { error: 'Falsche Email oder Passwort.' });
|
||||
|
||||
const match = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!match) return res.render('login', { error: 'Falsche Email oder Passwort.' });
|
||||
|
||||
req.session.userId = user._id;
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
// Logout
|
||||
app.get('/logout', (req, res) => {
|
||||
req.session.destroy(() => {
|
||||
res.redirect('/login');
|
||||
req.session.destroy(() => res.redirect('/login'));
|
||||
});
|
||||
|
||||
// Hauptseite
|
||||
app.get('/', requireLogin, (req, res) => res.render('index'));
|
||||
|
||||
// API-Routen (Beispiel)
|
||||
app.post('/api/save', requireLogin, async (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); // als Integer speichern
|
||||
|
||||
try {
|
||||
const doc = {
|
||||
espId,
|
||||
sensorNumber,
|
||||
name: name || '',
|
||||
description: description || '',
|
||||
address: address || '',
|
||||
createdAt: new Date()
|
||||
};
|
||||
await entriesCollection.insertOne(doc);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Fehler beim Speichern' });
|
||||
}
|
||||
});
|
||||
|
||||
// Neuer Endpoint zum Bearbeiten
|
||||
app.put('/api/update/:id', requireLogin, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
let { espId, sensorNumber, name, description, address } = req.body;
|
||||
|
||||
if (!espId || !sensorNumber) {
|
||||
return res.json({ error: 'ESP-ID und Sensornummer sind Pflichtfelder' });
|
||||
}
|
||||
|
||||
sensorNumber = parseInt(sensorNumber, 10);
|
||||
|
||||
try {
|
||||
await entriesCollection.updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: { espId, sensorNumber, name, description, address } }
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Fehler beim Aktualisieren' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/list', requireLogin, async (req, res) => {
|
||||
const { id } = req.query;
|
||||
if (id) {
|
||||
try {
|
||||
const item = await entriesCollection.findOne({ _id: new ObjectId(id) });
|
||||
if (item) return res.json([item]);
|
||||
return res.json([]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
try {
|
||||
const items = await entriesCollection.find({})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
res.json(items);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Fehler beim Laden' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/delete/:id', requireLogin, async (req, res) => {
|
||||
await entriesCollection.deleteOne({ _id: new ObjectId(req.params.id) });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Dummy-Funktion - hier später Logik implementieren
|
||||
function getAddress(sensorNumber) {
|
||||
|
||||
// Beispiel: feste Testwerte oder Datenbank-Logik
|
||||
const addressMap = {
|
||||
1001: 'Musterstraße 1, 12345 Musterstadt',
|
||||
2002: 'Beispielweg 5, 54321 Beispielstadt'
|
||||
};
|
||||
return addressMap[sensorNumber] || 'Adresse nicht gefunden';
|
||||
}
|
||||
|
||||
// .env (optional): ADDRESS_SERVICE_URL=https://noise.fuerst-stuttgart.de/srv/getaddress
|
||||
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) => {
|
||||
const sensorNumber = parseInt(req.params.sensorNumber, 10);
|
||||
if (isNaN(sensorNumber)) {
|
||||
return res.status(400).json({ error: 'Ungültige Sensornummer' });
|
||||
}
|
||||
|
||||
const url = `https://noise.fuerst-stuttgart.de/srv/getaddress?sensorid=${encodeURIComponent(sensorNumber)}`;
|
||||
|
||||
try {
|
||||
const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
||||
if (!r.ok) {
|
||||
return res.status(502).json({ error: `Adressdienst Fehler (${r.status})` });
|
||||
}
|
||||
|
||||
const data = await r.json();
|
||||
// Erwartete Struktur: { erg: { address: { street, plz, city } } }
|
||||
const addrObj = data?.erg?.address || data?.address || {};
|
||||
|
||||
const street = addrObj.street ?? '';
|
||||
const plz = addrObj.plz ?? '';
|
||||
const city = addrObj.city ?? '';
|
||||
|
||||
const rightPart = [plz, city].filter(Boolean).join(' ').trim();
|
||||
const addressString = [street, rightPart].filter(Boolean).join(', ');
|
||||
|
||||
return res.json({
|
||||
address: addressString, // <- vom Client direkt ins Inputfeld
|
||||
parts: { street, plz, city } // optional, falls du es später brauchst
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Address lookup failed:', err);
|
||||
return res.status(504).json({ error: 'Adressdienst nicht erreichbar' });
|
||||
}
|
||||
});
|
||||
|
||||
// Hauptseite jetzt mit Login-Schutz
|
||||
app.get('/', requireLogin, (req, res) => {
|
||||
res.render('index');
|
||||
});
|
||||
|
||||
// --- API ---
|
||||
app.post('/api/save', async (req, res) => {
|
||||
const { espId, sensorNumber } = req.body;
|
||||
if (!espId || !sensorNumber) {
|
||||
return res.status(400).json({ ok: false, error: 'ESP-ID und Sensornummer sind erforderlich' });
|
||||
}
|
||||
try {
|
||||
const doc = {
|
||||
espId: String(espId).trim(),
|
||||
sensorNumber: String(sensorNumber).trim(),
|
||||
createdAt: new Date()
|
||||
};
|
||||
const result = await entriesCollection.insertOne(doc);
|
||||
return res.json({ ok: true, id: result.insertedId, entry: doc });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ ok: false, error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/list', async (req, res) => {
|
||||
const page = Math.max(1, parseInt(req.query.page) || 1);
|
||||
const limit = Math.max(1, Math.min(100, parseInt(req.query.limit) || 50));
|
||||
try {
|
||||
const total = await entriesCollection.countDocuments();
|
||||
const rawItems = await entriesCollection.find({})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip((page - 1) * limit)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
const items = rawItems.map(it => ({
|
||||
...it,
|
||||
createdAt: formatDate(it.createdAt)
|
||||
}));
|
||||
return res.json({ ok: true, page, limit, total, items });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ ok: false, error: 'DB Fehler' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/entry/:id', async (req, res) => {
|
||||
try {
|
||||
await entriesCollection.deleteOne({ _id: new ObjectId(req.params.id) });
|
||||
return res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ ok: false });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server läuft auf http://localhost:${PORT}`);
|
||||
});
|
||||
app.listen(PORT, () => console.log(`Server läuft auf http://localhost:${PORT}`));
|
||||
@@ -2,32 +2,47 @@ doctype html
|
||||
html(lang="de")
|
||||
head
|
||||
meta(charset="utf-8")
|
||||
meta(name="viewport" content="width=device-width,initial-scale=1")
|
||||
title ESP-ID + Sensornummer speichern
|
||||
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 speichern
|
||||
h1 ESP-ID → Sensornummer
|
||||
div.card
|
||||
form#entryForm
|
||||
label(for="espId") ESP-ID:
|
||||
input#espId(type="text")
|
||||
|
||||
.card
|
||||
label(for="espId") ESP-ID:
|
||||
input#espId(placeholder="z.B. esp-1234")
|
||||
br
|
||||
br
|
||||
label(for="sensorNumber") Sensornummer:
|
||||
input#sensorNumber(placeholder="z.B. 42")
|
||||
br
|
||||
br
|
||||
button#saveBtn Speichern
|
||||
div#result
|
||||
label(for="sensorNumber") Sensornummer:
|
||||
input#sensorNumber(type="text" placeholder="Nur Zahlen erlaubt")
|
||||
|
||||
.card
|
||||
h2 Gespeicherte Einträge
|
||||
div.controls
|
||||
button#refreshBtn Aktualisieren
|
||||
| Seite:
|
||||
input#page(value="1")
|
||||
| Limit:
|
||||
input#limit(value="25")
|
||||
div#list
|
||||
label(for="name") Bezeichnung:
|
||||
input#name(type="text")
|
||||
|
||||
script(src="/global.js")
|
||||
label(for="description") Beschreibung:
|
||||
textarea#description
|
||||
|
||||
label(for="address") Anschrift:
|
||||
input#address(type="text" placeholder="Wird automatisch ausgefüllt, kann geändert werden")
|
||||
|
||||
button#saveBtn(type="button") Speichern
|
||||
div#result
|
||||
|
||||
div.controls
|
||||
button#refreshBtn Aktualisieren
|
||||
| Seite:
|
||||
input#page(value="1")
|
||||
| Limit:
|
||||
input#limit(value="10")
|
||||
|
||||
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")
|
||||
@@ -2,20 +2,18 @@ doctype html
|
||||
html(lang="de")
|
||||
head
|
||||
meta(charset="utf-8")
|
||||
meta(name="viewport" content="width=device-width,initial-scale=1")
|
||||
meta(name="viewport", content="width=device-width, initial-scale=1")
|
||||
title Login
|
||||
link(rel="stylesheet", href="/styles.css")
|
||||
body
|
||||
h1 Login
|
||||
if error
|
||||
p.error #{error}
|
||||
form(method="POST", action="/login")
|
||||
label(for="email") Email:
|
||||
input#email(type="email", name="email", required)
|
||||
br
|
||||
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)
|
||||
br
|
||||
button(type="submit") Einloggen
|
||||
p
|
||||
a(href="/register") Registrieren
|
||||
input#password(type="password" name="password" required)
|
||||
button(type="submit") Login
|
||||
if error
|
||||
p.error= error
|
||||
script(type="module" src="/login.js")
|
||||
@@ -2,20 +2,18 @@ doctype html
|
||||
html(lang="de")
|
||||
head
|
||||
meta(charset="utf-8")
|
||||
meta(name="viewport" content="width=device-width,initial-scale=1")
|
||||
title Registrierung
|
||||
meta(name="viewport", content="width=device-width, initial-scale=1")
|
||||
title Registrieren
|
||||
link(rel="stylesheet", href="/styles.css")
|
||||
body
|
||||
h1 Registrierung
|
||||
if error
|
||||
p.error #{error}
|
||||
form(method="POST", action="/register")
|
||||
label(for="email") Email:
|
||||
input#email(type="email", name="email", required)
|
||||
br
|
||||
form(method="POST" action="/register")
|
||||
label(for="email") E-Mail:
|
||||
input#email(type="email" name="email" required)
|
||||
span#emailStatus
|
||||
label(for="password") Passwort:
|
||||
input#password(type="password", name="password", required)
|
||||
br
|
||||
input#password(type="password" name="password" required)
|
||||
button(type="submit") Registrieren
|
||||
p
|
||||
a(href="/login") Zum Login
|
||||
if error
|
||||
p.error= error
|
||||
script(type="module" src="/register.js")
|
||||
Reference in New Issue
Block a user