Files
dickendock/backend/src/db.js

250 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 calendar_feeds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
url TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#4ecdc4',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
parent_id INTEGER REFERENCES folders(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS folder_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
folder_id INTEGER NOT NULL REFERENCES folders(id) ON DELETE CASCADE,
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(folder_id, shared_with)
);
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
originalname TEXT NOT NULL,
mimetype TEXT NOT NULL DEFAULT 'application/octet-stream',
size INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS file_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(file_id, shared_with)
);
CREATE TABLE IF NOT EXISTS admin_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
bemerkung TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'warteliste',
custom_price REAL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
calculation_id INTEGER REFERENCES calculations(id) ON DELETE SET NULL,
calc_name TEXT NOT NULL,
preis_freundschaft REAL NOT NULL,
preis_normal REAL NOT NULL,
preis_auftrag REAL NOT NULL,
stueckzahl INTEGER NOT NULL DEFAULT 1
);
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
// Default admin settings
const settingDefaults = [
['file_max_size_mb', '50'],
['file_max_count', '20'],
['file_allowed_ext', '.pdf,.jpg,.jpeg,.png,.gif,.zip,.txt,.docx,.xlsx,.mp4,.stl,.3mf'],
];
for (const [key, value] of settingDefaults) {
if (!db.prepare('SELECT key FROM admin_settings WHERE key=?').get(key))
db.prepare('INSERT INTO admin_settings (key,value) VALUES (?,?)').run(key, value);
}
if (db.prepare('SELECT COUNT(*) c FROM users').get().c === 0) {
db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)')
.run('admin', bcrypt.hashSync('admin123', 12), 'admin');
console.log('✅ Erster Start: Admin angelegt: admin / admin123 → Bitte Passwort sofort ändern!');
}
// ── Migrationen (werden bei jedem Start geprüft) ─────────────────────────────
const cols = db.prepare("PRAGMA table_info(calculations)").all().map(r => r.name);
if (!cols.includes('image')) db.exec("ALTER TABLE calculations ADD COLUMN image TEXT");
if (!cols.includes('bemerkung')) db.exec("ALTER TABLE calculations ADD COLUMN bemerkung TEXT NOT NULL DEFAULT ''");
// Migrationen orders
const ordCols = db.prepare("PRAGMA table_info(orders)").all().map(r => r.name);
if (!ordCols.includes('bezahlt')) db.exec("ALTER TABLE orders ADD COLUMN bezahlt INTEGER NOT NULL DEFAULT 0");
if (!ordCols.includes('bezahlt_am')) db.exec("ALTER TABLE orders ADD COLUMN bezahlt_am DATETIME");
// Migrationen order_items
// files migrations
const fileCols = db.prepare("PRAGMA table_info(files)").all().map(r => r.name);
if (!fileCols.includes('folder_id')) db.exec("ALTER TABLE files ADD COLUMN folder_id INTEGER REFERENCES folders(id) ON DELETE SET NULL");
// folder_shares migrations (only if table exists)
const fsTableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='folder_shares'").get();
if (fsTableExists) {
const fsCols = db.prepare("PRAGMA table_info(folder_shares)").all().map(r => r.name);
if (!fsCols.includes('created_at')) db.exec("ALTER TABLE folder_shares ADD COLUMN created_at DATETIME DEFAULT NULL");
}
const oiCols = db.prepare("PRAGMA table_info(order_items)").all().map(r => r.name);
if (!oiCols.includes('stunden')) db.exec("ALTER TABLE order_items ADD COLUMN stunden REAL NOT NULL DEFAULT 0");
if (!oiCols.includes('custom_price')) db.exec("ALTER TABLE order_items ADD COLUMN custom_price REAL");
if (!oiCols.includes('status')) db.exec("ALTER TABLE order_items ADD COLUMN status TEXT NOT NULL DEFAULT 'warteliste'");
if (!oiCols.includes('qty_warteliste')) db.exec("ALTER TABLE order_items ADD COLUMN qty_warteliste INTEGER NOT NULL DEFAULT 0");
if (!oiCols.includes('qty_in_arbeit')) db.exec("ALTER TABLE order_items ADD COLUMN qty_in_arbeit INTEGER NOT NULL DEFAULT 0");
if (!oiCols.includes('qty_fertig')) db.exec("ALTER TABLE order_items ADD COLUMN qty_fertig INTEGER NOT NULL DEFAULT 0");
// Bestehende Einträge: alle Stücke auf Warteliste setzen falls noch keine Qtys gesetzt
db.exec("UPDATE order_items SET qty_warteliste=stueckzahl WHERE qty_warteliste=0 AND qty_in_arbeit=0 AND qty_fertig=0");
// Migration: file_shares Passwort und Zugriffszeit
const fShareCols = db.prepare("PRAGMA table_info(file_shares)").all().map(r => r.name);
if (fShareCols.length && !fShareCols.includes('password_hash'))
db.exec("ALTER TABLE file_shares ADD COLUMN password_hash TEXT DEFAULT NULL");
if (fShareCols.length && !fShareCols.includes('accessed_at'))
db.exec("ALTER TABLE file_shares ADD COLUMN accessed_at DATETIME DEFAULT NULL");
// ── Nachrichten-Migrationen ───────────────────────────────────────────────────
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages'").get()) {
db.exec(`
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
recipient_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
encrypted_content TEXT NOT NULL,
iv TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
read_by_recipient INTEGER NOT NULL DEFAULT 0
)
`);
}
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='user_keys'").get()) {
db.exec(`
CREATE TABLE user_keys (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
public_key TEXT NOT NULL,
updated_at DATETIME DEFAULT NULL
)
`);
}
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='pushover_settings'").get()) {
db.exec(`
CREATE TABLE pushover_settings (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
user_key TEXT NOT NULL,
app_token TEXT NOT NULL
)
`);
}
// Migration: Soft-Delete-Spalten entfernen (nur Nachrichten übernehmen die noch sichtbar waren)
const msgCols = db.prepare("PRAGMA table_info(messages)").all().map(r => r.name);
if (msgCols.includes('deleted_by_sender')) {
db.exec(`
CREATE TABLE messages_v2 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
recipient_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
encrypted_content TEXT NOT NULL,
iv TEXT NOT NULL,
created_at DATETIME DEFAULT NULL,
read_by_recipient INTEGER NOT NULL DEFAULT 0
);
INSERT INTO messages_v2 (id, sender_id, recipient_id, encrypted_content, iv, created_at, read_by_recipient)
SELECT id, sender_id, recipient_id, encrypted_content, iv, created_at,
COALESCE(read_by_recipient, 0)
FROM messages
WHERE deleted_by_sender = 0 AND deleted_by_recipient = 0;
DROP TABLE messages;
ALTER TABLE messages_v2 RENAME TO messages;
`);
console.log('✅ Migration: messages → Hard-Delete (Soft-Delete-Spalten entfernt)');
}
// Falls read_by_recipient noch fehlt (sehr alte Installation)
const msgCols2 = db.prepare("PRAGMA table_info(messages)").all().map(r => r.name);
if (msgCols2.length && !msgCols2.includes('read_by_recipient')) {
db.exec("ALTER TABLE messages ADD COLUMN read_by_recipient INTEGER NOT NULL DEFAULT 0");
}
module.exports = db;