Dicken Dock v1.0.0
This commit is contained in:
71
backend/src/db.js
Normal file
71
backend/src/db.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
|
||||
const db = new Database(process.env.DB_PATH || '/data/dickendock.db');
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Dashboard-Widgets
|
||||
CREATE TABLE IF NOT EXISTS quick_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '🔗',
|
||||
sort_order INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
done INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Tool: 3D-Kalkulator
|
||||
CREATE TABLE IF NOT EXISTS calculations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
gramm REAL NOT NULL,
|
||||
stunden REAL NOT NULL,
|
||||
farben INTEGER NOT NULL DEFAULT 1,
|
||||
materialpreis_pro_gramm REAL NOT NULL DEFAULT 0.01,
|
||||
stromverbrauch_kw REAL NOT NULL DEFAULT 0.15,
|
||||
strompreis_pro_kwh REAL NOT NULL DEFAULT 0.38,
|
||||
druckerpreis REAL NOT NULL DEFAULT 550,
|
||||
gesamtdruckstunden REAL NOT NULL DEFAULT 5000,
|
||||
verschleiss_pro_stunde REAL NOT NULL DEFAULT 0.06,
|
||||
preis_freundschaft REAL NOT NULL,
|
||||
preis_normal REAL NOT NULL,
|
||||
preis_auftrag REAL NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// Standard-Admin beim ersten Start anlegen
|
||||
if (!db.prepare('SELECT id FROM users WHERE username = ?').get('admin')) {
|
||||
db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)')
|
||||
.run('admin', bcrypt.hashSync('admin123', 12), 'admin');
|
||||
console.log('✅ Admin angelegt: admin / admin123 → Bitte Passwort sofort ändern!');
|
||||
}
|
||||
|
||||
module.exports = db;
|
||||
34
backend/src/index.js
Normal file
34
backend/src/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
require('./db'); // Datenbank + Tabellen initialisieren
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// ── Core-Routen ───────────────────────────────────────────────────────────────
|
||||
app.use('/api/auth', require('./routes/auth'));
|
||||
app.use('/api/admin', require('./routes/admin'));
|
||||
app.use('/api/dashboard', require('./routes/dashboard'));
|
||||
app.use('/api/system', require('./routes/system'));
|
||||
|
||||
// ── Tool-Routen automatisch laden ─────────────────────────────────────────────
|
||||
// Neues Tool: Datei unter src/tools/<name>/routes.js ablegen → wird automatisch eingebunden
|
||||
const toolsDir = path.join(__dirname, 'tools');
|
||||
fs.readdirSync(toolsDir, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory())
|
||||
.forEach(d => {
|
||||
const routeFile = path.join(toolsDir, d.name, 'routes.js');
|
||||
if (fs.existsSync(routeFile)) {
|
||||
app.use(`/api/tools/${d.name}`, require(routeFile));
|
||||
console.log(` 🔧 Tool geladen: ${d.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ── React-Frontend ausliefern ─────────────────────────────────────────────────
|
||||
const PUBLIC = path.join(__dirname, '../public');
|
||||
app.use(express.static(PUBLIC));
|
||||
app.get('*', (_req, res) => res.sendFile(path.join(PUBLIC, 'index.html')));
|
||||
|
||||
app.listen(4000, () => console.log('🚀 Dicken Dock läuft auf Port 4000'));
|
||||
16
backend/src/middleware/auth.js
Normal file
16
backend/src/middleware/auth.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const SECRET = process.env.JWT_SECRET || 'dev-secret';
|
||||
|
||||
function authenticate(req, res, next) {
|
||||
const h = req.headers.authorization;
|
||||
if (!h?.startsWith('Bearer ')) return res.status(401).json({ error: 'Nicht eingeloggt' });
|
||||
try { req.user = jwt.verify(h.slice(7), SECRET); next(); }
|
||||
catch { res.status(401).json({ error: 'Token ungültig' }); }
|
||||
}
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
if (req.user?.role !== 'admin') return res.status(403).json({ error: 'Kein Zugriff' });
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { authenticate, requireAdmin };
|
||||
30
backend/src/routes/admin.js
Normal file
30
backend/src/routes/admin.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const multer = require('multer');
|
||||
const { authenticate, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
const DB_PATH = process.env.DB_PATH || '/data/dickendock.db';
|
||||
const upload = multer({ dest: '/tmp/' });
|
||||
|
||||
// GET /api/admin/backup
|
||||
router.get('/backup', authenticate, requireAdmin, (req, res) => {
|
||||
if (!fs.existsSync(DB_PATH)) return res.status(404).json({ error: 'DB nicht gefunden' });
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
res.download(DB_PATH, `dickendock-backup-${date}.db`);
|
||||
});
|
||||
|
||||
// POST /api/admin/restore
|
||||
router.post('/restore', authenticate, requireAdmin, upload.single('database'), (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'Keine Datei erhalten' });
|
||||
try {
|
||||
if (fs.existsSync(DB_PATH)) fs.copyFileSync(DB_PATH, DB_PATH + '.bak');
|
||||
fs.copyFileSync(req.file.path, DB_PATH);
|
||||
fs.unlinkSync(req.file.path);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
37
backend/src/routes/auth.js
Normal file
37
backend/src/routes/auth.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const db = require('../db');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
const SECRET = process.env.JWT_SECRET || 'dev-secret';
|
||||
|
||||
// POST /api/auth/login
|
||||
router.post('/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||
if (!user || !bcrypt.compareSync(password, user.password_hash))
|
||||
return res.status(401).json({ error: 'Benutzername oder Passwort falsch' });
|
||||
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, username: user.username, role: user.role },
|
||||
SECRET, { expiresIn: '7d' }
|
||||
);
|
||||
res.json({ token, user: { id: user.id, username: user.username, role: user.role } });
|
||||
});
|
||||
|
||||
// POST /api/auth/change-password
|
||||
router.post('/change-password', authenticate, (req, res) => {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id);
|
||||
if (!bcrypt.compareSync(currentPassword, user.password_hash))
|
||||
return res.status(400).json({ error: 'Aktuelles Passwort falsch' });
|
||||
if (!newPassword || newPassword.length < 8)
|
||||
return res.status(400).json({ error: 'Mindestens 8 Zeichen erforderlich' });
|
||||
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?')
|
||||
.run(bcrypt.hashSync(newPassword, 12), req.user.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
70
backend/src/routes/dashboard.js
Normal file
70
backend/src/routes/dashboard.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const express = require('express');
|
||||
const db = require('../db');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
const uid = req => req.user.id;
|
||||
|
||||
// ── Quick Links ───────────────────────────────────────────────────────────────
|
||||
router.get('/links', authenticate, (req, res) => {
|
||||
res.json(db.prepare('SELECT * FROM quick_links WHERE user_id=? ORDER BY sort_order,id').all(uid(req)));
|
||||
});
|
||||
router.post('/links', authenticate, (req, res) => {
|
||||
const { title, url, icon = '🔗' } = req.body;
|
||||
if (!title || !url) return res.status(400).json({ error: 'Titel und URL erforderlich' });
|
||||
const max = db.prepare('SELECT MAX(sort_order) m FROM quick_links WHERE user_id=?').get(uid(req));
|
||||
const r = db.prepare('INSERT INTO quick_links (user_id,title,url,icon,sort_order) VALUES (?,?,?,?,?)')
|
||||
.run(uid(req), title, url, icon, (max?.m ?? -1) + 1);
|
||||
res.json(db.prepare('SELECT * FROM quick_links WHERE id=?').get(r.lastInsertRowid));
|
||||
});
|
||||
router.put('/links/:id', authenticate, (req, res) => {
|
||||
const ex = db.prepare('SELECT * FROM quick_links WHERE id=? AND user_id=?').get(req.params.id, uid(req));
|
||||
if (!ex) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
const { title=ex.title, url=ex.url, icon=ex.icon } = req.body;
|
||||
db.prepare('UPDATE quick_links SET title=?,url=?,icon=? WHERE id=? AND user_id=?')
|
||||
.run(title, url, icon, req.params.id, uid(req));
|
||||
res.json(db.prepare('SELECT * FROM quick_links WHERE id=?').get(req.params.id));
|
||||
});
|
||||
router.delete('/links/:id', authenticate, (req, res) => {
|
||||
const r = db.prepare('DELETE FROM quick_links WHERE id=? AND user_id=?').run(req.params.id, uid(req));
|
||||
if (!r.changes) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// ── Todos ─────────────────────────────────────────────────────────────────────
|
||||
router.get('/todos', authenticate, (req, res) => {
|
||||
res.json(db.prepare('SELECT * FROM todos WHERE user_id=? ORDER BY done,sort_order,id').all(uid(req)));
|
||||
});
|
||||
router.post('/todos', authenticate, (req, res) => {
|
||||
const { text } = req.body;
|
||||
if (!text?.trim()) return res.status(400).json({ error: 'Text erforderlich' });
|
||||
const max = db.prepare('SELECT MAX(sort_order) m FROM todos WHERE user_id=?').get(uid(req));
|
||||
const r = db.prepare('INSERT INTO todos (user_id,text,sort_order) VALUES (?,?,?)')
|
||||
.run(uid(req), text.trim(), (max?.m ?? -1) + 1);
|
||||
res.json(db.prepare('SELECT * FROM todos WHERE id=?').get(r.lastInsertRowid));
|
||||
});
|
||||
router.put('/todos/:id', authenticate, (req, res) => {
|
||||
const ex = db.prepare('SELECT * FROM todos WHERE id=? AND user_id=?').get(req.params.id, uid(req));
|
||||
if (!ex) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
const text = req.body.text ?? ex.text;
|
||||
const done = req.body.done !== undefined ? (req.body.done ? 1 : 0) : ex.done;
|
||||
db.prepare('UPDATE todos SET text=?,done=? WHERE id=? AND user_id=?').run(text, done, req.params.id, uid(req));
|
||||
res.json(db.prepare('SELECT * FROM todos WHERE id=?').get(req.params.id));
|
||||
});
|
||||
router.delete('/todos/:id', authenticate, (req, res) => {
|
||||
const r = db.prepare('DELETE FROM todos WHERE id=? AND user_id=?').run(req.params.id, uid(req));
|
||||
if (!r.changes) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// ── Notizen ───────────────────────────────────────────────────────────────────
|
||||
router.get('/note', authenticate, (req, res) => {
|
||||
res.json(db.prepare('SELECT * FROM notes WHERE user_id=?').get(uid(req)) || { content: '', updated_at: null });
|
||||
});
|
||||
router.put('/note', authenticate, (req, res) => {
|
||||
db.prepare(`INSERT INTO notes (user_id,content,updated_at) VALUES (?,?,CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(user_id) DO UPDATE SET content=excluded.content,updated_at=CURRENT_TIMESTAMP`)
|
||||
.run(uid(req), req.body.content ?? '');
|
||||
res.json({ success: true, updated_at: new Date().toISOString() });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
40
backend/src/routes/system.js
Normal file
40
backend/src/routes/system.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
let VERSION = 'v1.0.0';
|
||||
try { VERSION = fs.readFileSync(path.join(__dirname, '../../version.txt'), 'utf8').trim(); } catch {}
|
||||
|
||||
const GITEA_URL = process.env.GITEA_URL;
|
||||
const GITEA_REPO = process.env.GITEA_REPO;
|
||||
|
||||
router.get('/version', authenticate, (_req, res) => res.json({ version: VERSION }));
|
||||
|
||||
router.get('/update-check', authenticate, async (_req, res) => {
|
||||
if (!GITEA_URL || !GITEA_REPO)
|
||||
return res.json({ hasUpdate: false, currentVersion: VERSION, configured: false });
|
||||
try {
|
||||
const r = await fetch(`${GITEA_URL}/api/v1/repos/${GITEA_REPO}/releases?limit=5`,
|
||||
{ signal: AbortSignal.timeout(5000) });
|
||||
const releases = await r.json();
|
||||
if (!Array.isArray(releases) || !releases.length)
|
||||
return res.json({ hasUpdate: false, currentVersion: VERSION, configured: true });
|
||||
res.json({
|
||||
hasUpdate: releases[0].tag_name !== VERSION,
|
||||
currentVersion: VERSION,
|
||||
latestVersion: releases[0].tag_name,
|
||||
configured: true,
|
||||
releases: releases.map(r => ({
|
||||
version: r.tag_name, name: r.name || r.tag_name,
|
||||
body: r.body || '(Keine Beschreibung)', publishedAt: r.published_at,
|
||||
})),
|
||||
});
|
||||
} catch (e) {
|
||||
res.json({ hasUpdate: false, currentVersion: VERSION, configured: true });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
57
backend/src/tools/kalkulator3d/routes.js
Normal file
57
backend/src/tools/kalkulator3d/routes.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// Tool: 3D-Druck-Kalkulator
|
||||
// Routen werden unter /api/tools/kalkulator3d/... eingebunden
|
||||
const express = require('express');
|
||||
const db = require('../../db');
|
||||
const { authenticate } = require('../../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', authenticate, (req, res) => {
|
||||
res.json(db.prepare('SELECT * FROM calculations WHERE user_id=? ORDER BY created_at DESC').all(req.user.id));
|
||||
});
|
||||
|
||||
router.post('/', authenticate, (req, res) => {
|
||||
const { name, gramm, stunden, farben,
|
||||
materialpreis_pro_gramm, stromverbrauch_kw, strompreis_pro_kwh,
|
||||
druckerpreis, gesamtdruckstunden, verschleiss_pro_stunde,
|
||||
preis_freundschaft, preis_normal, preis_auftrag } = req.body;
|
||||
if (!name || gramm == null || stunden == null)
|
||||
return res.status(400).json({ error: 'Name, Gramm und Stunden erforderlich' });
|
||||
const r = db.prepare(`INSERT INTO calculations
|
||||
(user_id,name,gramm,stunden,farben,materialpreis_pro_gramm,stromverbrauch_kw,
|
||||
strompreis_pro_kwh,druckerpreis,gesamtdruckstunden,verschleiss_pro_stunde,
|
||||
preis_freundschaft,preis_normal,preis_auftrag)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
|
||||
.run(req.user.id, name, gramm, stunden, farben,
|
||||
materialpreis_pro_gramm, stromverbrauch_kw, strompreis_pro_kwh,
|
||||
druckerpreis, gesamtdruckstunden, verschleiss_pro_stunde,
|
||||
preis_freundschaft, preis_normal, preis_auftrag);
|
||||
res.json(db.prepare('SELECT * FROM calculations WHERE id=?').get(r.lastInsertRowid));
|
||||
});
|
||||
|
||||
router.put('/:id', authenticate, (req, res) => {
|
||||
const ex = db.prepare('SELECT * FROM calculations WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||||
if (!ex) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
const { name, gramm, stunden, farben,
|
||||
materialpreis_pro_gramm, stromverbrauch_kw, strompreis_pro_kwh,
|
||||
druckerpreis, gesamtdruckstunden, verschleiss_pro_stunde,
|
||||
preis_freundschaft, preis_normal, preis_auftrag } = req.body;
|
||||
db.prepare(`UPDATE calculations SET
|
||||
name=?,gramm=?,stunden=?,farben=?,materialpreis_pro_gramm=?,stromverbrauch_kw=?,
|
||||
strompreis_pro_kwh=?,druckerpreis=?,gesamtdruckstunden=?,verschleiss_pro_stunde=?,
|
||||
preis_freundschaft=?,preis_normal=?,preis_auftrag=?,updated_at=CURRENT_TIMESTAMP
|
||||
WHERE id=? AND user_id=?`)
|
||||
.run(name, gramm, stunden, farben,
|
||||
materialpreis_pro_gramm, stromverbrauch_kw, strompreis_pro_kwh,
|
||||
druckerpreis, gesamtdruckstunden, verschleiss_pro_stunde,
|
||||
preis_freundschaft, preis_normal, preis_auftrag,
|
||||
req.params.id, req.user.id);
|
||||
res.json(db.prepare('SELECT * FROM calculations WHERE id=?').get(req.params.id));
|
||||
});
|
||||
|
||||
router.delete('/:id', authenticate, (req, res) => {
|
||||
const r = db.prepare('DELETE FROM calculations WHERE id=? AND user_id=?').run(req.params.id, req.user.id);
|
||||
if (!r.changes) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user