Adresse wird aufgelöst

Optik augehübscht
This commit is contained in:
2025-08-13 15:44:29 +00:00
parent 2a9bcb50f7
commit 1da23e24c7
9 changed files with 600 additions and 295 deletions

417
server.js
View File

@@ -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}`));