Initial commit: Aquarium Logbuch React/Vite App
- React 18 + Vite Frontend - Node.js/Express Backend - Vollstaendige Logbuch-Funktionalitaet fuer Aquarien - Deploy-Script fuer aqualog CT 211 (192.168.0.246)
This commit is contained in:
226
backend/server.js
Normal file
226
backend/server.js
Normal file
@@ -0,0 +1,226 @@
|
||||
"use strict";
|
||||
const express = require("express");
|
||||
const bcrypt = require("bcrypt");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const app = express();
|
||||
const PORT = 3001;
|
||||
const DATA_FILE = "/var/lib/aquarium/data.json";
|
||||
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(48).toString("hex");
|
||||
const SALT_ROUNDS = 12;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// ─── Data helpers ──────────────────────────────────────────────────────────
|
||||
function loadData() {
|
||||
try {
|
||||
if (fs.existsSync(DATA_FILE)) return JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
|
||||
} catch {}
|
||||
return { users: [], aquariums: [], entries: [], reminders: [] };
|
||||
}
|
||||
function saveData(d) {
|
||||
const dir = path.dirname(DATA_FILE);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(d, null, 2));
|
||||
}
|
||||
|
||||
// ─── Middleware ────────────────────────────────────────────────────────────
|
||||
function auth(req, res, next) {
|
||||
const h = req.headers.authorization;
|
||||
if (!h || !h.startsWith("Bearer ")) return res.status(401).json({ error: "Nicht angemeldet" });
|
||||
try {
|
||||
req.user = jwt.verify(h.slice(7), JWT_SECRET);
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: "Sitzung abgelaufen" });
|
||||
}
|
||||
}
|
||||
function admin(req, res, next) {
|
||||
if (req.user.role !== "admin") return res.status(403).json({ error: "Admin-Rechte erforderlich" });
|
||||
next();
|
||||
}
|
||||
function ownsAquarium(d, aqId, user) {
|
||||
const aq = d.aquariums.find(a => a.id === aqId);
|
||||
if (!aq) return null;
|
||||
if (aq.userId === user.id || user.role === "admin") return aq;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Init admin ────────────────────────────────────────────────────────────
|
||||
function initAdmin() {
|
||||
const d = loadData();
|
||||
if (!d.users.find(u => u.username === "aqlab")) {
|
||||
d.users.push({
|
||||
id: "admin-1",
|
||||
username: "aqlab",
|
||||
passwordHash: bcrypt.hashSync("D4sP4sswortBek0mmtKe1ner!", SALT_ROUNDS),
|
||||
role: "admin",
|
||||
displayName: "Administrator",
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
saveData(d);
|
||||
console.log("[aquarium] Admin-Benutzer 'aqlab' angelegt.");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Auth routes ───────────────────────────────────────────────────────────
|
||||
app.post("/api/auth/login", (req, res) => {
|
||||
const { username, password } = req.body || {};
|
||||
if (!username || !password) return res.status(400).json({ error: "Benutzername und Passwort erforderlich" });
|
||||
const d = loadData();
|
||||
const u = d.users.find(x => x.username === username);
|
||||
if (!u || !bcrypt.compareSync(password, u.passwordHash))
|
||||
return res.status(401).json({ error: "Ungültiger Benutzername oder Passwort" });
|
||||
const token = jwt.sign({ id: u.id, username: u.username, role: u.role, displayName: u.displayName }, JWT_SECRET, { expiresIn: "14d" });
|
||||
res.json({ token, user: { id: u.id, username: u.username, role: u.role, displayName: u.displayName } });
|
||||
});
|
||||
|
||||
app.get("/api/auth/me", auth, (req, res) => res.json(req.user));
|
||||
|
||||
app.post("/api/auth/change-password", auth, (req, res) => {
|
||||
const { currentPassword, newPassword } = req.body || {};
|
||||
if (!currentPassword || !newPassword) return res.status(400).json({ error: "Felder erforderlich" });
|
||||
if (newPassword.length < 6) return res.status(400).json({ error: "Passwort mindestens 6 Zeichen" });
|
||||
const d = loadData();
|
||||
const u = d.users.find(x => x.id === req.user.id);
|
||||
if (!u || !bcrypt.compareSync(currentPassword, u.passwordHash))
|
||||
return res.status(401).json({ error: "Aktuelles Passwort falsch" });
|
||||
u.passwordHash = bcrypt.hashSync(newPassword, SALT_ROUNDS);
|
||||
saveData(d);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ─── User management (admin) ───────────────────────────────────────────────
|
||||
app.get("/api/users", auth, admin, (req, res) => {
|
||||
const d = loadData();
|
||||
res.json(d.users.map(u => ({ id: u.id, username: u.username, role: u.role, displayName: u.displayName, createdAt: u.createdAt })));
|
||||
});
|
||||
|
||||
app.post("/api/users", auth, admin, (req, res) => {
|
||||
const { username, password, role = "user", displayName = "" } = req.body || {};
|
||||
if (!username || !password) return res.status(400).json({ error: "Benutzername und Passwort erforderlich" });
|
||||
if (password.length < 6) return res.status(400).json({ error: "Passwort mindestens 6 Zeichen" });
|
||||
const d = loadData();
|
||||
if (d.users.find(u => u.username === username)) return res.status(409).json({ error: "Benutzername bereits vergeben" });
|
||||
const u = { id: `user-${Date.now()}`, username, passwordHash: bcrypt.hashSync(password, SALT_ROUNDS), role, displayName: displayName || username, createdAt: new Date().toISOString() };
|
||||
d.users.push(u);
|
||||
saveData(d);
|
||||
res.status(201).json({ id: u.id, username: u.username, role: u.role, displayName: u.displayName, createdAt: u.createdAt });
|
||||
});
|
||||
|
||||
app.put("/api/users/:id", auth, admin, (req, res) => {
|
||||
const d = loadData();
|
||||
const u = d.users.find(x => x.id === req.params.id);
|
||||
if (!u) return res.status(404).json({ error: "Benutzer nicht gefunden" });
|
||||
if (req.body.displayName !== undefined) u.displayName = req.body.displayName;
|
||||
if (req.body.role !== undefined && u.id !== "admin-1") u.role = req.body.role;
|
||||
if (req.body.password && req.body.password.length >= 6) u.passwordHash = bcrypt.hashSync(req.body.password, SALT_ROUNDS);
|
||||
saveData(d);
|
||||
res.json({ id: u.id, username: u.username, role: u.role, displayName: u.displayName });
|
||||
});
|
||||
|
||||
app.delete("/api/users/:id", auth, admin, (req, res) => {
|
||||
if (req.params.id === "admin-1") return res.status(403).json({ error: "Admin-Benutzer kann nicht gelöscht werden" });
|
||||
if (req.params.id === req.user.id) return res.status(403).json({ error: "Eigener Account kann nicht gelöscht werden" });
|
||||
const d = loadData();
|
||||
const idx = d.users.findIndex(u => u.id === req.params.id);
|
||||
if (idx === -1) return res.status(404).json({ error: "Benutzer nicht gefunden" });
|
||||
d.users.splice(idx, 1);
|
||||
saveData(d);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ─── Aquarium routes ───────────────────────────────────────────────────────
|
||||
app.get("/api/aquariums", auth, (req, res) => {
|
||||
const d = loadData();
|
||||
const list = req.user.role === "admin"
|
||||
? d.aquariums
|
||||
: d.aquariums.filter(a => a.userId === req.user.id);
|
||||
res.json(list);
|
||||
});
|
||||
|
||||
app.post("/api/aquariums", auth, (req, res) => {
|
||||
const { name, volume = 60, description = "" } = req.body || {};
|
||||
if (!name?.trim()) return res.status(400).json({ error: "Name erforderlich" });
|
||||
const d = loadData();
|
||||
const aq = { id: `aq-${Date.now()}`, userId: req.user.id, name: name.trim(), volume: Number(volume) || 60, description, createdAt: new Date().toISOString() };
|
||||
d.aquariums.push(aq);
|
||||
saveData(d);
|
||||
res.status(201).json(aq);
|
||||
});
|
||||
|
||||
app.put("/api/aquariums/:id", auth, (req, res) => {
|
||||
const d = loadData();
|
||||
const aq = ownsAquarium(d, req.params.id, req.user);
|
||||
if (!aq) return res.status(404).json({ error: "Aquarium nicht gefunden" });
|
||||
if (req.body.name?.trim()) aq.name = req.body.name.trim();
|
||||
if (req.body.volume !== undefined) aq.volume = Number(req.body.volume) || aq.volume;
|
||||
if (req.body.description !== undefined) aq.description = req.body.description;
|
||||
saveData(d);
|
||||
res.json(aq);
|
||||
});
|
||||
|
||||
app.delete("/api/aquariums/:id", auth, (req, res) => {
|
||||
const d = loadData();
|
||||
const aq = ownsAquarium(d, req.params.id, req.user);
|
||||
if (!aq) return res.status(404).json({ error: "Aquarium nicht gefunden" });
|
||||
d.aquariums = d.aquariums.filter(a => a.id !== req.params.id);
|
||||
d.entries = d.entries.filter(e => e.aquariumId !== req.params.id);
|
||||
d.reminders = (d.reminders || []).filter(r => r.aquariumId !== req.params.id);
|
||||
saveData(d);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ─── Entry routes ──────────────────────────────────────────────────────────
|
||||
app.get("/api/aquariums/:aqId/entries", auth, (req, res) => {
|
||||
const d = loadData();
|
||||
if (!ownsAquarium(d, req.params.aqId, req.user)) return res.status(404).json({ error: "Aquarium nicht gefunden" });
|
||||
const entries = d.entries
|
||||
.filter(e => e.aquariumId === req.params.aqId)
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
res.json(entries);
|
||||
});
|
||||
|
||||
app.post("/api/aquariums/:aqId/entries", auth, (req, res) => {
|
||||
const d = loadData();
|
||||
if (!ownsAquarium(d, req.params.aqId, req.user)) return res.status(404).json({ error: "Aquarium nicht gefunden" });
|
||||
const entry = { id: `e-${Date.now()}`, aquariumId: req.params.aqId, date: new Date().toISOString(), values: req.body.values || {}, note: req.body.note || "" };
|
||||
d.entries.push(entry);
|
||||
saveData(d);
|
||||
res.status(201).json(entry);
|
||||
});
|
||||
|
||||
app.delete("/api/aquariums/:aqId/entries/:id", auth, (req, res) => {
|
||||
const d = loadData();
|
||||
if (!ownsAquarium(d, req.params.aqId, req.user)) return res.status(404).json({ error: "Aquarium nicht gefunden" });
|
||||
const idx = d.entries.findIndex(e => e.id === req.params.id && e.aquariumId === req.params.aqId);
|
||||
if (idx === -1) return res.status(404).json({ error: "Eintrag nicht gefunden" });
|
||||
d.entries.splice(idx, 1);
|
||||
saveData(d);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ─── Reminder routes ───────────────────────────────────────────────────────
|
||||
app.get("/api/aquariums/:aqId/reminder", auth, (req, res) => {
|
||||
const d = loadData();
|
||||
if (!ownsAquarium(d, req.params.aqId, req.user)) return res.status(404).json({ error: "Aquarium nicht gefunden" });
|
||||
const r = (d.reminders || []).find(x => x.aquariumId === req.params.aqId) || null;
|
||||
res.json(r);
|
||||
});
|
||||
|
||||
app.put("/api/aquariums/:aqId/reminder", auth, (req, res) => {
|
||||
const d = loadData();
|
||||
if (!ownsAquarium(d, req.params.aqId, req.user)) return res.status(404).json({ error: "Aquarium nicht gefunden" });
|
||||
if (!d.reminders) d.reminders = [];
|
||||
const idx = d.reminders.findIndex(r => r.aquariumId === req.params.aqId);
|
||||
const r = { aquariumId: req.params.aqId, intervalDays: req.body.intervalDays ?? 7, lastChange: req.body.lastChange ?? new Date().toISOString(), enabled: req.body.enabled ?? true };
|
||||
idx >= 0 ? (d.reminders[idx] = r) : d.reminders.push(r);
|
||||
saveData(d);
|
||||
res.json(r);
|
||||
});
|
||||
|
||||
initAdmin();
|
||||
app.listen(PORT, "127.0.0.1", () => console.log(`[aquarium] API läuft auf :${PORT}`));
|
||||
Reference in New Issue
Block a user