250 lines
12 KiB
JavaScript
250 lines
12 KiB
JavaScript
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;
|